\chapter{文件系统库}\label{ch20}
直到C++17，Boost.Filesystem库终于被C++标准采纳。
在这个过程中，这个库用新的语言特性进行了很多调整、改进了和其他库的一致性、
进行了精简、还扩展了很多缺失的功能（例如计算两个文件系统路径之间的相对路径）。


\section{基本的示例}
让我们以一些基本的示例开始。

\subsection{打印文件系统路径类的属性}
下面的程序允许我们传递一个字符串作为文件系统路径，然后根据给定路径的文件类型打印出一些信息：
\inputcodefile{filesystem/checkpath1.cpp}
我们首先把传入的命令行参数转换为了一个文件系统路径：
\begin{lstlisting}
    std::filesystem::path p{argv[1]};   // p代表一个文件系统路径（有可能不存在）
\end{lstlisting}
然后，我们进行了下列检查：
\begin{itemize}
    \item 如果该路径代表一个普通文件，我们打印出它的大小：
    \begin{lstlisting}
    if (is_regular_file(p)) {   // 路径p是普通文件吗？
        std::cout << p << " exists with " << file_size(p) << " bytes\n";
    }
    \end{lstlisting}
    像下面这样调用程序：
    \begin{blacklisting}
    checkpath checkpath.cpp
    \end{blacklisting}
    将会有如下输出：
    \begin{blacklisting}
    "checkpath.cpp" exists with 907 bytes
    \end{blacklisting}
    注意输出路径时会自动把路径名用双引号括起来输出
    （把路径用双引号括起来、反斜杠用另一个反斜杠转义，
    \hyperref[ch20.1.1.1]{对Windows路径来说是一个问题}）。
    \item 如果路径是一个目录，我们遍历这个目录中的所有文件并打印出这些文件的路径：
    \begin{lstlisting}
    if (is_directory(p)) {      // 路径p是目录吗？
        std::cout << p << " is a directory containing:\n";
        for (auto& e : std::filesystem::directory_iterator{p}) {
            std::cout << "  " << e.path() << '\n';
        }
    }
    \end{lstlisting}
    这里，我们使用了\texttt{directory\_iterator}，
    它提供了\texttt{begin()}和\texttt{end()}，所以我们可以使用范围for循环来遍历
    \texttt{directory\_entry}元素。在这里，我们使用了\texttt{directory\_entry}的
    成员函数\texttt{path()}，返回该目录项的文件系统路径。
    像下面这样调用程序：
    \begin{blacklisting}
    checkpath .
    \end{blacklisting}
    输出将是：
    \begin{blacklisting}
    "." is a directory containing:
    "./checkpath.cpp"
    "./checkpath.exe"
    ...
    \end{blacklisting}
    \item 最后，我们检查传入的文件系统路径是否不存在：
    \begin{lstlisting}
    if (exists(p)) {        // 路径p存在吗？
        ...
    }
    \end{lstlisting}
\end{itemize}
注意根据\emph{参数依赖查找(argument dependent lookup)(ADL)}，
你不需要使用完全限定的名称来调用\texttt{is\_regular\_\\
file()}、\texttt{file\_size()}、\texttt{is\_directory()}、\texttt{exists()}等函数。
它们都属于命名空间\texttt{std::filesystem}，但是因为它们的参数也属于这个命名空间，
所以调用它们时会自动在这个命名空间中进行查找。

\subsubsection{在Windows下处理路径}\label{ch20.1.1.1}
（译者注：可能是水平有限，完全看不懂作者在这一小节的逻辑，
所以只能按照自己的理解胡乱翻译，如有错误请见谅。）

默认情况下，输出路径时用双引号括起来并用反斜杠转义反斜杠在Windows
下会导致一个问题。在Windows下以如下方式调用程序：
\begin{blacklisting}
    checkpath C:\
\end{blacklisting}
将会有如下输出：
\begin{blacklisting}
    "C:\\" is a directory containing:
    ...
    "C:\\Users"
    "C:\\Windows"
\end{blacklisting}
用双引号括起来输出路径可以确保输出的文件名可以被直接复制粘贴到其他程序里，
并且经过转义之后还会恢复为原本的文件名。
然而，终端通常不接受这样的路径。

因此，一个在Windows下的可移植版本应该使用成员函数\texttt{string()}，
这样可以在向标准输出写入路径时避免输出双引号：
\inputcodefile{filesystem/checkpath2.cpp}
现在，在Windows上以如下方式调用程序：
\begin{blacklisting}
    checkpath C:\
\end{blacklisting}
将会有如下输出：
\begin{blacklisting}
    "C:\" is a directory containing:
    ...
    "C:\Users"
    "C:\Windows"
\end{blacklisting}
有一些\hyperref[ch20.3.4]{其他转换}可以把路径转换为通用格式
或者把string转换为本地编码。

\subsection{用\texttt{switch}语句处理不同的文件系统类型}
我们可以像下面这样修改并改进上面的例子：
\inputcodefile{filesystem/checkpath3.cpp}

\subsubsection{命名空间\texttt{fs}}
首先，我们做了一个非常普遍的操作：我们定义了\texttt{fs}作为命名空间
\texttt{std::filesystem}的缩写：
\begin{lstlisting}
    namespace fs = std::filesystem;
\end{lstlisting}
使用这个新初始化的命名空间的一个例子是下面\texttt{switch}语句中的路径\texttt{p}：
\begin{lstlisting}
    fs::path p{argv[1]};
\end{lstlisting}
这里的\texttt{switch}语句使用了新的\nameref{ch2.2}特性，
初始化路径的同时把路径的类型作为分支条件：
\begin{lstlisting}
    switch (fs::path p{argv[1]}; status(p).type()) {
        ...
    }
\end{lstlisting}
表达式\texttt{status(p).type()}首先创建了一个\texttt{file\_status}对象，
然后该对象的\texttt{type()}方法返回了一个\\
\texttt{file\_type}类型的值。
通过这种方式我们可以直接处理不同的类型，而不需要使用\texttt{is\_regular\_file()}、\\
\texttt{is\_directory()}等函数构成的if-else链。
我们通过多个步骤（先调用\texttt{status()}再调用\texttt{type()}）才得到了最后的类型，
因此我们不需要为不感兴趣的其他信息付出多余的系统调用开销。

注意可能已经有特定实现的\texttt{file\_type}存在。例如，
Windows就提供了\hyperref[junction]{特殊的文件类型\texttt{junction}}。
然而，使用了它的代码是不可移植的。

\subsection{创建不同类型的文件}\label{ch20.1.3}
在介绍了文件系统的只读操作之后，让我们给出首个进行修改的例子。
下面的程序在一个子目录\texttt{tmp}中创建了不同类型的文件：
\inputcodefile{filesystem/createfiles.cpp}
让我们一步步来分析这段程序。

\subsubsection{命名空间\texttt{fs}}
首先，我们又一次定义了\texttt{fs}作为命名空间\texttt{std::filesystem}的缩写：
\begin{lstlisting}
    namespace fs = std::filesystem;
\end{lstlisting}
之后我们使用这个命名空间为临时文件创建了一个基本的子目录：
\begin{lstlisting}
    fs::path testDir{"tmp/test"};
\end{lstlisting}

\subsubsection{创建目录}
当我们尝试创建子目录时：
\begin{lstlisting}
    create_directories(testDir);
\end{lstlisting}
通过使用\texttt{create\_directories()}我们可以递归创建整个路径中所有缺少的目录
（还有一个\texttt{create\_\\
directory()}只在已存在的目录中创建目录）。

当目标目录已经存在时这个调用并不会返回错误。
然而，其他的问题会导致错误并抛出一个相应的异常。

如果\texttt{testDir}已经存在，\texttt{create\_directories()}会返回\texttt{false}。
因此，你可以这么写：
\begin{lstlisting}
    if (!create_directories(testDir)) {
        std::cout << "\"" << testDir.string() << "\" already exists\n";
    }
\end{lstlisting}

\subsubsection{创建普通文件}
之后我们用一些内容创建了一个新文件\texttt{tmp/test/data.txt}：
\begin{lstlisting}
    auto testFile = testDir / "data.txt";
    std::ofstream dataFile{testFile};
    if (!dataFile) {
        std::cerr << "OOPS, can't open \"" << testFile.string() << "\"\n";
        std::exit(EXIT_FAILURE);  // 失败退出程序
    }
    dataFile << "The answer is 42\n";
\end{lstlisting}
这里，我们使用了运算符\texttt{/}来扩展路径，然后传递给文件流的构造函数。
如你所见，普通文件的创建可以使用现有的I/O流库来实现。
然而，I/O流的构造函数多了一个以文件系统路径为参数的版本
（一些函数例如\texttt{open()}也添加了这种重载版本）。

注意你仍然应该总是检查创建/打开文件的操作是否成功了。
这里有很多种可能发生的错误（\hyperref[ch20.1.3.6]{见下文}）。

\subsubsection{创建符号链接}
接下来的语句尝试创建符号链接\texttt{tmp/slink}指向目录\texttt{tmp/test}：
\begin{lstlisting}
    create_directory_symlink("test", testDir.parent_path() / "slink");
\end{lstlisting}
注意第一个参数的路径是以即将创建的符号链接所在的目录为起点的相对路径。
因此，你必须传递\texttt{"test"}而不是\texttt{"tmp/test"}来高效的
创建链接\texttt{tmp/slink}指向\texttt{tmp/test}。如果你调用：
\begin{lstlisting}
    std::filesystem::create_directory_symlink("tmp/test", "tmp/slink");
\end{lstlisting}
你将会高效的创建符号链接\texttt{tmp/slink}，然而它会指向\texttt{tmp/tmp/test}。

注意通常情况下，也可以调用\texttt{create\_symlink()}代替\texttt{create\_
directory\_symlink()}来创建目录的符号链接。
然而，一些操作系统可能对目录的符号链接有特殊处理或者当知道要创建的符号链接指向目录时会有优化，
因此，当你想创建指向目录的符号链接时你应该使用\texttt{create\_directory\_symlink()}。

最后，注意这个调用在Windows上可能会失败并导致\hyperref[创建链接失败]{错误处理}，
因为创建符号链接可能需要管理员权限。

\subsubsection{递归遍历目录}\label{ch20.1.3.5}
最后，我们递归地遍历了当前目录：
\begin{lstlisting}
    auto iterOpts = fs::directory_options::follow_directory_symlink;
    for (auto& e : fs::recursive_directory_iterator(".", iterOpts)) {
        std::cout << "  " << e.path().lexically_normal().string() << '\n';
    }
\end{lstlisting}
注意我们使用了一个递归目录迭代器并传递了选项\texttt{follow\_directory\_symlink}来
遍历符号链接。因此，我们在POSIX兼容系统上可能会得到类似于如下输出：
\begin{blacklisting}
    /home/nico:
    ...
    tmp
    tmp/slink
    tmp/slink/data.txt
    tmp/test
    tmp/test/data.txt
    ...
\end{blacklisting}
在Windows系统上有类似如下输出：
\begin{blacklisting}
    C:\Users\nico:
    ...
    tmp
    tmp\slink
    tmp\slink\data.txt
    tmp\test
    tmp\test\data.txt
    ...
\end{blacklisting}
注意在我们打印目录项之前调用了\texttt{lexically\_normal()}。
如果略过这一步，目录项的路径可能会包含一个前缀，
这个前缀是创建目录迭代器时传递的实参。
因此，在循环内直接打印路径：
\begin{lstlisting}
    auto iterOpts = fs::directory_options::follow_directory_symlink;
    for (auto& e : fs::recursive_directory_iterator(".", iterOpts)) {
        std::cout << "  " << e.path() << '\n';
    }
\end{lstlisting}
将会在POSIX兼容系统上有如下输出：
\begin{blacklisting}
    all files:
    ...
    "./testdir"
    "./testdir/data.txt"
    "./tmp"
    "./tmp/test"
    "./tmp/test/data.txt"
\end{blacklisting}
在Windows上，输出将是：
\begin{blacklisting}
    all files:
    ...
    ".\\testdir"
    ".\\testdir\\data.txt"
    ".\\tmp"
    ".\\tmp\\test"
    ".\\tmp\\test\\data.txt"
\end{blacklisting}
通过调用\texttt{lexically\_normal()}我们可以得到正规化的路径，
它移除了前导的代表当前路径的点。还有，\hyperref[ch20.1.1.1]{如上文所述}，
通过调用\texttt{string()}我们避免了输出路径时用双引号括起来。
这里没有调用\texttt{string()}的输出结果在POSIX兼容的系统上看起来OK（只是路径两端有双引号），
但在Windows上的结果看起来就很奇怪（因为每一个反斜杠都需要反斜杠转义）。

\subsubsection{错误处理}\label{ch20.1.3.6}
文件系统往往是麻烦的根源。你可能因为在文件名中使用了无效的字符而导致操作失败，
或者当你正在访问文件系统时它已经被其他程序修改了。
因此，根据平台和权限的不同，这个程序中可能会有很多问题。

对于那些没有被返回值覆盖的情况（例如当目录已经存在时），
我们捕获了相应的异常并打印了一般的信息和第一个路径：
\begin{lstlisting}
    try {
        ...
    }
    catch (const fs::filesystem_error& e) {
        std::cerr << "EXCEPTION: " << e.what() << '\n';
        std::cerr << "    path1: \"" << e.path1().string() << "\"\n";
    }
\end{lstlisting}
例如，如果我们不能创建目录，将会打印出类似于如下消息：
\begin{blacklisting}
    EXCEPTION: filesystem error: cannot create directory: [tmp/test]
    path1: "tmp/test"
\end{blacklisting}
如果我们不能创建符号链接可能是因为它已经存在了，
或者我们可能需要特殊权限，这些情况下你可能会得到如下消息：\label{创建链接失败}
\begin{blacklisting}
    EXCEPTION: create_directory_symlink: Can't create a file when it already exists:
                                         "tmp\test\data.txt", "testdir"
        path1: "tmp\test\data.txt"
\end{blacklisting}
或者：
\begin{blacklisting}
    EXCEPTION: create_directory_symlink: A requied privilege is not held by the
                                         client.: "test", "tmp\slink"
        path1: "test"
\end{blacklisting}
在每一种情况下，都要注意在多用户/多进程操作系统中情况可能会在任何时候改变，
这意味着你刚刚创建的目录甚至可能已经被删除、重命名、或已经被同名的文件覆盖。
因此，很显然不能只根据当前的情况就保证一个预期操作一定是有效的。
最好的方式就是尝试做想做的操作（例如，创建目录、打开文件）
并处理抛出的异常和错误，或者验证预期的行为。

然而，有些时候文件系统操作能正常执行但不是按你预想的结果。
例如，如果你想在指定目录中创建一个文件并且已经有了一个和目录同名的指向另一个目录的符号链接，
那么这个文件可能在一个预料之外的地方创建或者覆写。
\footnote{译者注：比如你想在当前目录下递归创建\texttt{"a/b"}，
但已经有了一个\texttt{"a"}是一个指向\texttt{"c"}目录的符号链接，
这时你实际会在目录\texttt{"c"}下创建\texttt{"b"}。}
这种情况是有可能的（用户完全有可能会创建目录的符号链接），但是如果你想检测这种情况，
在创建文件之前你需要\nameref{ch20.4.1.1}（这可能比你一开始想的要复杂很多）。

再强调一次：文件系统并不保证进行处理之前的检查的结果直到你进行处理时仍然有效。

\subsection{使用并行算法处理文件系统}
参见\hyperref[ch22.6.1.4]{dirsize.cpp}查看另一个使用并行算法计算目录树中所有文件大小之和的例子。


\section{原则和术语}
在讨论文件系统库的细节之前，我们不得不继续介绍一些设计原则和术语。
这是必须的，因为标准库要覆盖不同的操作系统并把系统提供的接口映射为公共的API。

\subsection{通用的可移植的分隔符}
C++标准库不仅标准化了所有操作系统的文件系统中公共的部分，在很多情况下，
C++标准还尽可能的遵循POSIX标准的要求来实现。
对于一些操作，只要是合理的就应该能正确执行，如果操作是不合理的，实现应该报错。
这些错误可能是：
\begin{itemize}
    \item 特殊的字符不能被用作文件名
    \item 创建了文件系统不支持的元素（例如，符号链接）
\end{itemize}
不同文件系统的差异也应该纳入考虑：
\begin{itemize}
    \item 大小写敏感：\\
    \texttt{"hello.txt"}和\texttt{"Hello.txt"}和\texttt{"hello.TXT"}可能指向
    同一个文件（Windows上）也可能指向三个不同的文件（POSIX兼容系统）。
    \item 绝对路径和相对路径：\\
    在某些系统上，\texttt{"/bin"}是一个绝对路径（POSIX兼容系统），
    然而在某些系统上不是（Windows）。
\end{itemize}

\subsection{命名空间}
文件系统库在\texttt{std}里有自己的子命名空间\texttt{filesystem}。
一个很常见的操作是定义缩写\texttt{fs}：
\begin{lstlisting}
    namespace fs = std::filesystem;
\end{lstlisting}
这允许我们使用\texttt{fs::current\_path()}代
替\texttt{std::filesystem::current\_path()}。

这一章的示例代码中将经常使用\texttt{fs}作为缩写。

注意你应该总是使用完全限定的函数调用，尽管不指明命名空间时
通过\emph{参数依赖查找(argument dependent lookup)(ADL)}也能够工作。
但如果不用命名空间限定有时可能\hyperref[ADL导致意外行为]{导致意外的行为}。

\subsection{文件系统路径}\label{ch20.2.3}
文件系统库的一个关键元素是\texttt{path}。它代表文件系统中某一个文件的位置。
它由可选的根名称、可选的根目录、和一些以目录分隔符分隔的文件名组成。
路径可以是相对的（此时文件的位置依赖于当前的工作目录）或者是绝对的。

路径可能有不同的格式：
\begin{itemize}
    \item 通用格式，这是可移植的
    \item 本地格式，这是底层文件系统特定的
\end{itemize}
在POSIX兼容系统上通用格式和本地格式没有什么区别。
在Windows上，通用格式\texttt{/tmp/test.txt}也是有效的本地格式，
另外\texttt{\textbackslash tmp\textbackslash test.txt}也是有效的
（\texttt{/tmp/test.txt}和\texttt{\textbackslash tmp\textbackslash test.txt}
是同一个路径的两种本地版本）。在OPenVMS上，相应的本地格式将是\texttt{[tmp]test.txt}。

也有一些特殊的文件名：
\begin{itemize}
    \item \texttt{"."}代表当前目录
    \item \texttt{".."}代表父目录
\end{itemize}
通用的路径格式如下：\\
\hspace*{2em}\texttt{[rootname] [rootdir] [relativepath]}\\
这里：
\begin{itemize}
    \item 可选的根名称是实现特定的（例如，在POSIX系统上可以是\texttt{//host}，
    而在Windows上可以是\texttt{C:}）
    \item 可选的根目录是一个目录分隔符
    \item 相对路径是若干目录分隔符分隔的文件名
\end{itemize}
目录分隔符由一个或多个\texttt{'/'}组成或者是实现特定的。

可移植的通用路径的例子有：
\begin{blacklisting}
    //host1/bin/hello.txt
    .
    tmp/
    /a/b//.../c
\end{blacklisting}
注意在POSIX系统上最后一个路径和\texttt{/a/c}指向同一个位置，并且都是绝对路径。
而在Windows上则是相对路径（因为没有指定驱动器/分区（盘））。

另一方面，\texttt{C:/bin}在Windows上是绝对路径（在\texttt{"C"}盘上的根目录\texttt{"bin"}），
但在POSIX系统上是一个相对路径（目录\texttt{"C:"}下的子目录\texttt{"bin"}）。

在Windows系统上，反斜杠是实现特定的目录分隔符，
因此上面的路径\emph{也}可以使用反斜杠作为目录分隔符：
\begin{blacklisting}
    \\host1\bin\hello.txt
    .
    tmp\
    \a\b\..\c
\end{blacklisting}
文件系统库提供了\hyperref[ch20.3.4]{在本地格式和通用格式之间转换的函数}。

一个\texttt{path}可能为空，这意味着没有定义路径。
这种状态的含义\emph{不}需要和\texttt{"."}一样。它的含义依赖于上下文。

\subsection{正规化}
路径可以进行正规化，在正规化的路径中：
\begin{itemize}
    \item 文件名由单个推荐的目录分隔符分隔。
    \item 除非整个路径就是\texttt{"."}（代表当前目录），否则路径中不会使用\texttt{"."}。
    \item 路径中除了开头以外的地方不会包含\texttt{".."}（不能在路径中上下徘徊）。
    \item 除非整个路径就是\texttt{"."}或者\texttt{".."}，否则当路径结尾的文件名是目录时要在最后加上目录分隔符。
\end{itemize}
注意正规化之后以目录分隔符结尾的路径和不以目录分隔符结尾的路径是不同的。
这是因为在某些操作系统中，当它们知道目标路径是一个目录时行为可能会发生改变
（例如，有尾部的分隔符时符号链接将被解析）。

表\hyperref[t20.1]{路径正规化的效果}列举了一些在POSIX系统和Windows系统上
对路径进行正规化的例子。注意再重复一次，在POSIX系统上，\texttt{C:bar}和\texttt{C:}只是
把冒号作为文件名的一部分的单个文件名，并没有特殊的含义，而在Windows上，它们指定了一个分区。
\begin{table}[htb]
    \centering
    \begin{tabular}{l|l|l}
        \hline
        \textbf{路径}                                    & \textbf{POSIX正规化}                              & \textbf{Windows正规化}                                               \\
        \hline
        \texttt{foo/.///bar/../}                       & \texttt{foo/}                                  & \texttt{foo\textbackslash}                                        \\
        \texttt{//host/../foo.txt}                     & \texttt{//host/foo.txt}                        & \texttt{\textbackslash \textbackslash host\textbackslash foo.txt} \\
        \texttt{./f/../.f/}                            & \texttt{.f/}                                   & \texttt{.f\textbackslash}                                         \\
        \texttt{C:bar/../}                             & \texttt{.}                                     & \texttt{C:}                                                       \\
        \texttt{C:/bar/..}                             & \texttt{C:/}                                   & \texttt{C:\textbackslash}                                         \\
        \texttt{C:\textbackslash bar\textbackslash ..} & \texttt{C:\textbackslash bar\textbackslash ..} & \texttt{C:\textbackslash}                                         \\
        \texttt{/./../data.txt}                        & \texttt{/data.txt}                             & \texttt{\textbackslash data.txt}                                  \\
        \texttt{././}                                  & \texttt{.}                                     & \texttt{.}                                                        \\
        \hline
    \end{tabular}
    \caption{路径正规化的效果}
    \label{t20.1}
\end{table}

注意路径\texttt{C:\textbackslash bar\textbackslash ..}在POSIX兼容系统上经过正规化
之后没有任何变化。原因是这些系统上反斜杠并不是目录分隔符，所以这整个路径只是一个
带有冒号、两个反斜杠、两个点的\emph{单个}文件名。

文件系统库同时提供了\hyperref[ch20.3.3]{词法正规化}（不访问文件系统）
和\hyperref[ch20.4.5]{依赖文件系统的正规化}两种方式的相关函数。

\subsection{成员函数VS独立函数}
文件系统库提供了一些函数，有些是成员函数有些是独立函数。这么做的目的是：
\begin{itemize}
    \item \textbf{成员函数开销较小}。这是因为它们是纯词法的操作，并不会访问实际的文件系统，
    这意味着它们不需要进行操作系统调用。例如：
    \begin{lstlisting}
    mypath.is_absolute()        // 检查路径是否是绝对的
    \end{lstlisting}
    \item \textbf{独立函数开销较大}。因为它们通常会访问实际的文件系统，
    这意味着需要进行操作系统调用。例如：
    \begin{lstlisting}
    equivalent(path1, path2);   // 如果两个路径指向同一个文件则返回true
    \end{lstlisting}
\end{itemize}
有时，文件系统库甚至为同一个功能既提供根据词法的版本又提供访问实际文件系统的版本：
\begin{lstlisting}
    std::filesystem::path fromP, toP;
    ...
    toP.lexically_relative(fromP);  // 返回从fromP到toP的词法路径
    relative(toP, fromP);           // 返回从fromP到toP的实际路径
\end{lstlisting}
得益于\emph{参数依赖查找(ADL)}，很多情况下当调用独立函数时你不需要指明完整命名空间
\texttt{std::filesystem}，只要参数是文件系统库里定义的类型。只有当用其它类型隐式转换
为参数时你才需要给出完全限定的函数名。例如：\label{ADL导致意外行为}
\begin{lstlisting}
    create_directory(std::filesystem::path{"tmpdir"});  // OK
    remove(std::filesystem::path{"tmpdir"});            // OK
    std::filesystem::create_directory("tmpdir");        // OK
    std::filesystem::remove("tmpdir");                  // OK
    create_directory("tmpdir");                         // ERROR
\end{lstlisting}
最后一个调用将会编译失败，因为我们并没有传递文件系统命名空间里的类型作为参数，
因此也不会在该命名空间里查找符号\texttt{create\_directory}。

然而，这里有一个著名的陷阱：
\begin{lstlisting}
    remove("tmpdir");   // OOPS：调用C函数remove()
\end{lstlisting}
根据你包含的头文件，这个调用可能会找到C函数\texttt{remove()}，
它的行为有一些不同：它也会删除指定的文件但不会删除空目录。

因此，强烈推荐使用完全限定的文件系统库里的函数名。例如：
\begin{lstlisting}
    namespace fs = std::filesystem;
    ...
    fs::remove("tmpdir");   // OK：调用C++文件系统库函数remove()
\end{lstlisting}

\subsection{错误处理}
\hyperref[ch20.1.3.6]{如上文所述}，文件系统是错误的根源。
你必须考虑相应的文件是否存在、文件操作是否被允许、该操作是否会违背资源限制。
另外，当程序运行时其它进程可能创建、修改、或者移除了某些文件，这意味着事先检查
并不能保证没有错误。

问题在于从理论上讲，你不能提前保证下一次文件系统操作能够成功。
任何事先检查的结果都可能在你实际进行处理时失效。
因此，最好的方法是在进行一个或多个文件系统操作时处理好相应的异常或者错误。

注意，当读写普通文件时，默认情况下I/O流并不会抛出异常或错误。
当操作遇到错误时它只会什么也不做。因此，建议至少检查一下文件是否被成功打开。

因为并不是所有情况下都适合抛出异常（例如当一个文件系统调用失败时你想直接处理），
所以文件系统库使用了混合的异常处理方式：
\begin{itemize}
    \item 默认情况下，文件系统错误会作为异常处理。
    \item 然而，如果你想的话可以在本地处理具体的某一个错误。
\end{itemize}
因此，文件系统库通常为每个操作提供两个重载版本：
\begin{enumerate}
    \item 默认情况下（没有额外的错误处理参数），出现错误时抛出\texttt{filesystem\_error}异常。
    \item 传递额外的输出参数时，可以得到一个错误码或错误信息，而不是异常。
\end{enumerate}
注意在第二种情况下，你可能会得到一个特殊的返回值来表示特定的错误。

\subsubsection{使用\texttt{filesystem\_error}异常}\label{ch20.2.6.1}
例如，你可以尝试像下面这样创建一个目录：
\begin{lstlisting}
    if (!create_directory(p)) { // 发生错误时抛出异常（除非错误是该路径已经存在）
        std::cout << p << " already exists\n";  // 该路径已经存在
    }
\end{lstlisting}
这里没有传递错误码参数，因此错误时通常会抛出异常。
然而，注意当目录已存在时这种特殊情况是直接返回\texttt{false}。
因此，只有当其他错误例如没有权限创建目录、路径\texttt{p}无效、
违反了文件系统限制（例如路径长度超过上限）时才会抛出异常。

可以直接或间接的用\texttt{try-catch}包含这段代码，
然后处理\texttt{std::filesystem::filesystem\_error}异常：
\begin{lstlisting}
    try {
        ...
        if (!create_directory(p)) { // 错误时抛出异常（除非错误是该路径已经存在）
            std::cout << p << " already exists\n"; // 该路径已经存在
        }
        ...
    }
    catch (const std::filesystem::filesystem_error& e) { // 派生自std::exception
        std::cout << "EXCEPTION: " << e.what() << '\n';
        std::cout << "     path: " << e.path1() << '\n';
    }
\end{lstlisting}
如你所见，文件系统异常提供了标准异常的\texttt{what()}函数API来返回一个
实现特定的错误信息。然而，API还提供了\texttt{path1()}来获取错误相关的第一个路径，
和\texttt{path2()}来获取相关的第二个路径。

\subsubsection{使用\texttt{error\_code}参数}\label{ch20.2.6.2}
另一种创建目录的方式如下所示：
\begin{lstlisting}
    std::error_code ec;
    create_directory(p, ec);    // 发生错误时设置错误码
    if (ec) {                   // 如果设置了错误码（因为发生了错误）
        std::cout << "ERROR: " << ec.message() << "\n";
    }
\end{lstlisting}
之后，我们还可以检查特定的错误码：
\begin{lstlisting}
    if (ec == std::errc::read_only_file_system) {   // 如果设置了特定的错误码
        std::cout << "ERROR: " << p << " is read-only\n";
    }
\end{lstlisting}
注意这种情况下，我们仍然必须检查\texttt{create\_directory()的返回值}：
\begin{lstlisting}
    std::error_code ec;
    if (!create_directory(p, ec)) { // 发生错误时设置错误码
        // 发生任何错误时
        std::cout << "can't create directory " << p << "\n";
        std::cout << "error: " << ec.message() << "\n";
    }
\end{lstlisting}
然而，并不是所有的文件系统操作都提供这种能力（因为它们在正常情况下会返回一些值）。

类型\texttt{error\_code}由C++11引入，它包含了一系列可移植的错误条件，例如
\texttt{std::errc::read\_only\_\\
filesystem}。在POSIX兼容的系统上这些被映射为\texttt{errno}的值。

\subsection{文件类型}\label{ch20.2.7}
不同的操作系统支持不同的文件类型。标准文件系统库中也考虑到了这一点，它定义了一个
枚举类型\texttt{file\_type}，标准中定义了如下的值：
\begin{lstlisting}
    namespace std::filesystem {
        enum class file_type {
            regular, directory, symlink,
            block, character, fifo, socket,
            ...
            none, not_found, unknown,
        };
    }
\end{lstlisting}
表\hyperref[t20.2]{\texttt{file\_type}的值}列出了这些值的含义。
\begin{table}[htb]
    \centering
    \begin{tabular}{l|l}
        \hline
        \textbf{值}          & \textbf{含义}   \\
        \hline
        \texttt{regular}    & 普通文件          \\
        \texttt{directory}  & 目录文件          \\
        \texttt{symlink}    & 符号链接文件        \\
        \texttt{character}  & 字符特殊文件        \\
        \texttt{block}      & 块特殊文件         \\
        \texttt{fifo}       & FIFO或者管道文件    \\
        \texttt{socket}     & 套接字文件         \\
        \ldots              & 附加的实现定义的文件类型  \\
        \texttt{none}       & 文件的类型未知       \\
        \texttt{unknown}    & 文件存在但推断不出类型   \\
        \texttt{not\_found} & 虚拟的表示文件不存在的类型 \\
        \hline
    \end{tabular}
    \caption{文件系统类型的值}
    \label{t20.2}
\end{table}

操作系统平台可能会提供附加的文件类型值。然而，使用它们是不可移植的。
例如，Windows就提供了文件类型值\texttt{junction}，它被用于NTFS文件系统中的
\emph{NTFS junctions}（也被称为软链接）。
它们被用作链接来访问同一台电脑上不同的子卷(盘)。\label{junction}

除了普通文件和目录之外，最常见的类型是符号链接，它是一种指向另一个位置的文件。
指向的位置可能有一个文件也可能没有。
注意有些操作系统和/或文件系统（例如FAT文件系统）完全不支持符号链接。
有些操作系统只支持普通文件的符号链接。
注意在Windows上需要特殊的权限才能创建符号链接，可以用\texttt{mklink}命令创建。

字符特殊文件、块特殊文件、FIFO、套接字都来自于UNIX文件系统。
目前，Visual C++并没有使用这四种类型中的任何一个。
\footnote{Windows管道的行为有些不同，也不被识别为\texttt{fifo}。}

如你所见，有一些特殊的值来表示文件不存在或者类型未知或者无法探测出类型。

在这一章的剩余部分我将使用两种广义的类型来代表相应的若干文件类型：\label{文件类型}
\begin{itemize}
    \item \emph{其他文件}：除了普通文件、目录、符号链接之外的所有类型的文件。
    库函数\texttt{is\_other()}和这个术语相匹配。
    \item \emph{特殊文件}：下列类型的文件：字符特殊文件、块特殊文件、FIFO、套接字。
\end{itemize}
另外，\emph{特殊文件}类型加上实现定义的文件类型就构成了\emph{其他文件}类型。


\section{路径操作}
有很多处理文件系统的操作。这些操作涉及的关键类型是\texttt{std::filesystem::path}，
它表示一个可能存在也可能不存在的文件的绝对或相对的路径。

你可以创建路径、检查路径、修改路径、比较路径。
因为这些操作一般都不会访问实际的文件系统（例如不会检查文件是否存在，也不会解析符号链接），
所以它们的开销很小。因此，它们通常被定义为成员函数（如果这些操作既不是构造函数也不是运算符的话）。

\subsection{创建路径}
表\hyperref[t20.3]{创建路径}列出了创建新的路径对象的方法。
\begin{table}[htb]
    \centering
    \begin{tabular}{l|l}
        \hline
        \textbf{调用}                      & \textbf{效果}      \\
        \hline
        \texttt{path\{charseq\}}         & 用一个字符序列初始化路径     \\
        \texttt{path\{beg, end\}}        & 用一个范围初始化路径       \\
        \texttt{u8path(u8string)}        & 用一个UTF-8字符串初始化路径 \\
        \texttt{current\_path()}         & 返回当前工作目录的路径      \\
        \texttt{temp\_directory\_path()} & 返回临时文件的路径        \\
        \hline
    \end{tabular}
    \caption{创建路径}
    \label{t20.3}
\end{table}

第一个构造函数以字符序列为参数，这里的字符序列代表一系列有效的方式：
\begin{itemize}
    \item 一个\texttt{string}
    \item 一个\texttt{string\_view}
    \item 一个以空字符结尾的字符数组
    \item 一个以空字符结尾的字符输入迭代器（指针）
\end{itemize}
注意\texttt{current\_path()}和\texttt{temp\_directory\_path()}都是开销较大的操作，
因为它们依赖于系统调用。如果给\texttt{current\_path()}传递一个参数，它也可以用来
\hyperref[ch20.4.6]{修改当前工作目录}。

通过\texttt{u8path()}你可以使用UTF-8字符串创建可移植的路径。例如：
\begin{lstlisting}
    // 将路径p初始化为"Köln"（Cologne的德语名）：
    std::filesystem::path p{std::filesystem::u8path(u8"K\u00F6ln")};
    ...

    // 用UTF-8字符串创建目录：
    std::string utf8String = readUTF8String(...);
    create_directory(std::filesystem::u8path(utf8String));
\end{lstlisting}

\subsection{检查路径}\label{ch20.3.2}
表\hyperref[t20.4]{检查路径}列出了检查路径\texttt{p}时可以调用的函数。
注意这些操作都不会访问底层的操作系统，因此都是\texttt{path}类的成员函数。
\begin{table}[htb]
    \centering
    \begin{tabular}{l|l}
        \hline
        \textbf{调用}                       & \textbf{效果}                 \\
        \hline
        \texttt{p.empty()}                & 返回路径是否为空                    \\
        \texttt{p.is\_absolute()}         & 返回路径是否是绝对的                  \\
        \texttt{p.is\_relative()}         & 返回路径是否是相对的                  \\
        \texttt{p.has\_filename()}        & 返回路径是否既不是目录也不是根名称           \\
        \texttt{p.has\_stem()}            & 和\texttt{has\_filename()}一样 \\
        \texttt{p.has\_extension()}       & 返回路径是否有扩展名                  \\
        \texttt{p.has\_root\_name()}      & 返回路径是否包含根名称                 \\
        \texttt{p.has\_root\_directory()} & 返回路径是否包含根目录                 \\
        \texttt{p.has\_root\_path()}      & 返回路径是否包含根名称或根目录             \\
        \texttt{p.has\_parent\_path()}    & 返回路径是否包含父路径                 \\
        \texttt{p.has\_relative\_path()}  & 返回路径是否不止包含根元素               \\
        \texttt{p.filename()}             & 返回文件名（或者空路径）                \\
        \texttt{p.stem()}                 & 返回没有扩展名的文件名（或者空路径）          \\
        \texttt{p.extension()}            & 返回扩展名（或者空路径）                \\
        \texttt{p.root\_name()}           & 返回根名称（或者空路径）                \\
        \texttt{p.root\_directory()}      & 返回根目录（或者空路径）                \\
        \texttt{p.root\_path()}           & 返回根元素（或者空路径）                \\
        \texttt{p.parent\_path()}         & 返回父路径（或者空路径）                \\
        \texttt{p.relative\_path()}       & 返回不带根元素的路径（或者空路径）           \\
        \texttt{p.begin()}                & 返回路径元素的起点                   \\
        \texttt{p.end()}                  & 返回路径元素的终点                   \\
        \hline
    \end{tabular}
    \caption{检查路径}
    \label{t20.4}
\end{table}

每一个路径要么是绝对的要么是相对的。如果没有根目录那么路径就是相对的
（相对路径也可能包含根名称；例如，\texttt{C:hello.txt}就是Windows下的一个相对路径）。

\texttt{has\_...()}函数等价于检查相应的没有\texttt{has\_}前缀的函数的返回值是否为空路径。

注意下面几点：
\begin{itemize}
    \item 如果路径含有根元素或者目录分隔符那么就包含父路径。如果路径只由根元素组成
    （也就是说相对路径为空），\texttt{parent\_path()}的返回值就是整个路径。
    也就是说，路径\texttt{"/"}的父路径还是\texttt{"/"}。只有纯文件名的路径例如
    \texttt{"hello.txt"}的父路径是空。
    \item 如果一个路径包含文件名那么一定包含stem（文件名中不带扩展名的部分）。
    \footnote{从C++17才开始这样，因为以前文件名可以只包含扩展名。}
    \item 空路径是相对路径（除了\texttt{empty()}和\texttt{is\_relative()}
    之外的操作都返回\texttt{false}或者空路径）。
\end{itemize}
这些操作的结果可能会依赖于操作系统。例如，路径
\texttt{C:/hello.txt}
\begin{itemize}
    \item 在Unix系统上
    \begin{itemize}
        \item 是相对路径
        \item 没有根元素（既没有根名称也没有根目录），因为\texttt{C:}只是一个文件名
        \item 有父路径\texttt{C:}
        \item 有相对路径\texttt{C:/hello.txt}
    \end{itemize}
    \item 在Windows系统上
    \begin{itemize}
        \item 是绝对的
        \item 有根名称\texttt{C:}和根目录\texttt{/}
        \item 沒有父路径
        \item 有相对路径\texttt{hello.txt}
    \end{itemize}
\end{itemize}

\subsubsection{遍历路径}
你可以遍历一个路径，这将会返回路径的所有元素：根名称（如果有的话）、根目录（如果有的话）、
所有的文件名。如果路径以目录分隔符结尾，最后的元素将是空文件名。
\footnote{在C++17之前，文件系统库使用\texttt{.}来表示结尾的目录分隔符。
更改这个行为是为了区分以路径分隔符结尾的路径和以路径分隔符后还有一个点结尾的路径。}

路径迭代器是双向迭代器，所以你可以递减它。迭代器的值的类型是\texttt{path}。
然而，两个在同一个路径上迭代的迭代器可能\emph{不}指向同一个\texttt{path}对象，
即使它们迭代到了相同的路径元素。

例如，考虑：
\begin{lstlisting}
    void printPath(const std::filesystem::path& p)
    {
        std::cout << "path elements of \"" << p.string() << "\":\n";
        for (std::filesystem::path elem : p) {
            std::cout << "  \"" << elem.string() << '"';
        }
        std::cout << '\n';
    }
\end{lstlisting}
和如下代码效果相同：
\begin{lstlisting}
    void printPath(const std::filesystem::path& p)
    {
        std::cout << "path elements of \"" << p.string() << "\":\n";
        for (auto pos = p.begin(); pos != p.end(); ++pos) {
            std::filesystem::path elem = *pos;
            std::cout << "  \"" << elem.string() << '"';
        }
        std::cout << '\n';
    }
\end{lstlisting}
如果像下面这样调用这个函数：
\begin{lstlisting}
    printPath("../sub/file.txt");
    printPath("/usr/tmp/test/dir/");
    printPath("C:\\usr\\tmp\\test\\dir\\");
\end{lstlisting}
在POSIX兼容系统上的输出将会是：
\begin{blacklisting}
    path elements of "../sub/file.txt":
    ".."  "sub"  "file.txt"
    path elements of "/usr/tmp/test/dir/":
    "/"  "usr"  "tmp"  "test"  "dir"  ""
    path elements of "C:\\usr\\tmp\\test\\dir\\":
    "C:\\usr\\tmp\\test\\dir\\"
\end{blacklisting}
注意最后一个路径只是一个文件名，因为在POSIX兼容系统上\texttt{C:}不是有效的根名称，
反斜杠也不是有效的目录分隔符。

在Windows上的输出将是：
\begin{blacklisting}
    path elements of "../sub/file.txt":
    ".."  "sub"  "file.txt"
    path elements of "/usr/tmp/test/dir/":
    "/"  "usr"  "tmp"  "test"  "dir"  ""
    path elements of "C:\usr\tmp\test\dir\":
    "C:"  "\"  "usr"  "tmp"  "test"  "dir"  ""
\end{blacklisting}
为了检查路径\texttt{p}是否以目录分隔符结尾，你可以这么写：
\begin{lstlisting}
    if (!p.empty() && (--p.end())->empty()) {
        std::cout << p << " has a trailing separator\n";
    }
\end{lstlisting}

\subsection{路径I/O和转换}\label{ch20.3.3}
表\hyperref[t20.5]{路径I/O和转换}列出了路径的读写操作和转换操作。
这些函数也不会访问实际的文件系统。如果必须要处理符号链接，
你可能需要使用\hyperref[ch20.4.5]{依赖文件系统的路径转换}。
\begin{table}[htb]
    \centering
    \begin{tabular}{l|l}
        \hline
        \textbf{调用}                         & \textbf{效果}                                     \\
        \hline
        \texttt{strm << p}                  & 用双引号括起来输出路径                                     \\
        \texttt{strm >> p}                  & 读取用双引号括起来的路径                                    \\
        \texttt{p.string()}                 & 以\texttt{std::string}返回路径                       \\
        \texttt{p.wstring()}                & 以\texttt{std::wstring}返回路径                      \\
        \texttt{p.u8string()}               & 以类型为\texttt{std::u8string}的UTF-8字符串返回路径         \\
        \texttt{p.u16string()}              & 以类型为\texttt{std::u16string}的UTF-16字符串返回路径       \\
        \texttt{p.u32string()}              & 以类型为\texttt{std::u32string}的UTF-32字符串返回路径       \\
        \texttt{p.string<...>()}            & 以\texttt{std::basic\_string<...>}返回路径           \\
        \texttt{p.lexically\_normal()}      & 返回正规化的路径                                        \\
        \texttt{p.lexically\_relative(p2)}  & 返回从\texttt{p2}到\texttt{p}的相对路径（如果没有则返回空路径）      \\
        \texttt{p.lexically\_proximate(p2)} & 返回从\texttt{p2}到\texttt{p}的路径（如果没有则返回\texttt{p}） \\
        \hline
    \end{tabular}
    \caption{路径I/O和转换}
    \label{t20.5}
\end{table}

\texttt{lexically\_...()}函数会返回一个新的路径，而其他的转换函数将返回相应的字符串类型。
所有这些函数都不会修改调用者的路径。

例如，下面的代码：
\begin{lstlisting}
    std::filesystem::path p{"/dir/./sub//sub1/../sub2"};
    std::cout <<  "path:               " << p << '\n';
    std::cout <<  "string():           " << p.string() << '\n';
    std::wcout << "wstring():          " << p.wstring() << '\n';
    std::cout <<  "lexically_normal(): " << p.lexically_normal() << '\n';
\end{lstlisting}
前三行的输出是相同的：
\begin{blacklisting}
    path:               "/dir/./sub//sub1/../sub2"
    string():           /dir/./sub//sub1/../sub2
    wstring():          /dir/./sub//sub1/../sub2
\end{blacklisting}
但最后一行的输出就依赖于目录分隔符了。在POSIX兼容系统上输出是：
\begin{blacklisting}
    lexically_normal(): "/dir/sub/sub2"
\end{blacklisting}
而在Windows上输出是：
\begin{blacklisting}
    lexically_normal(): "\\dir\\sub\\sub2"
\end{blacklisting}

\subsubsection{路径I/O}
首先，注意I/O运算符以双引号括起来的字符串方式读写路径。
你可以把它们转换为字符串来避免双引号：
\begin{lstlisting}
    std::filesystem::path file{"test.txt"};
    std::cout << file << '\n';          // 输出："test.txt"
    std::cout << file.string() << '\n'; // 输出：test.txt
\end{lstlisting}
在Windows上，情况可能会更糟糕。下面的代码：
\begin{lstlisting}
    std::filesystem::path tmp{"C:\\Windows\\Temp"};
    std::cout << tmp << '\n';
    std::cout << tmp.string() << '\n';
    std::cout << '"' << tmp.string() << "\"\n";
\end{lstlisting}
将会有如下输出：
\begin{blacklisting}
    "C:\\Windows\\Temp"
    C:\Windows\Temp
    "C:\Windows\Temp"
\end{blacklisting}
注意读取路径时既支持带双引号的字符串也支持不带双引号的字符串。
因此，所有的输出形式都能使用输入运算符再读取回来：
\begin{lstlisting}
    std::filesystem::path tmp;
    std::cin >> tmp;    // 读取有双引号和无双引号的路径
\end{lstlisting}

\subsubsection{正规化}
当你处理可移植代码时正规化可能会导致更多令人惊奇的结果。例如：
\begin{lstlisting}
    std::filesystem::path p2{"//host\\dir/sub\\/./\\"};
    // 译者注：此处原文是
    // std::filesystem::path p2{"//dir\\subdir/subsubdir\\/./\\"};
    // 应是作者笔误

    std::cout << "p2:                 " << p2 << '\n';
    std::cout << "lexically_normal(): " << p2.lexically_normal() << '\n';
\end{lstlisting}
在Windows系统上可能会有如下输出：
\begin{blacklisting}
    p2:                 "//host\\dir/sub\\/./\\"
    lexically_normal(): "\\\\host\\dir\\sub\\"
\end{blacklisting}
然而，在POSIX兼容系统上，输出将是：
\begin{blacklisting}
    p2:                 "//host\\dir/sub\\/./\\"
    lexically_normal(): "/host\\dir/sub\\/\\"
\end{blacklisting}
原因是对于POSIX兼容系统来说反斜杠既不是路径分隔符也不是有效的根名称，
这意味着我们得到了一个有三个文件名的绝对路径，三个文件名分别是\texttt{host\textbackslash dir}、
\texttt{sub\textbackslash}、\texttt{\textbackslash}。
在POSIX兼容系统上，没有办法把反斜杠作为目录分隔符处理
（\hyperref[ch20.3.4]{\texttt{generic\_string()}和\texttt{make\_preferred()}}也没有用）。
因此，对于可移植的代码，当处理路径时你应该总是使用通用路径格式。

但是，当\hyperref[遍历.]{遍历当前目录}时使用\texttt{lexically\_normal()}
移除开头的点是个好方法。

\subsubsection{相对路径}
\texttt{lexically\_relative()}和\texttt{lexically\_proximate()}都可以被用来计算
两个路径间的相对路径。不同之处在于如果没有相对路径时的行为，
只有当一个是相对路径一个是绝对路径或者两个路径的根名称不同时才会发生这种情况。
这种情况下：
\begin{itemize}
    \item 对于\texttt{p.lexically\_relative(p2)}，如果没有从\texttt{p2}到\texttt{p}
    的相对路径，将会返回空路径。
    \item 对于\texttt{p.lexically\_proximate(p2)}，如果没有从\texttt{p2}到\texttt{p}
    的相对路径，将会返回\texttt{p}。
\end{itemize}
因为这两个操作都是词法操作，所以不会考虑实际的文件系统（可能会有符号链接）和\texttt{current\_path()}。
如果两个路径相同，相对路径将是\texttt{"."}。例如：
\begin{lstlisting}
    fs::path{"/a/d"}.lexically_relative("/a/b/c");      // "../../d"
    fs::path{"/a/b/c"}.lexically_relative("/a/d");      // "../b/c"
    fs::path{"/a/b"}.lexically_relative("/a/b");        // "."
    fs::path{"/a/b"}.lexically_relative("/a/b/");       // "."
    fs::path{"/a/b"}.lexically_relative("/a/b\\");      // "."
    fs::path{"/a/b"}.lexically_relative("/a/d/../c");   // "../b"
    fs::path{"a/d/../b"}.lexically_relative("a/c");     // "../d/../b"
    fs::path{"a//d/..//b"}.lexically_relative("a/c");   // "../d/../b"
\end{lstlisting}
在Windows平台上，则是：
\begin{lstlisting}
    fs::path{"C:/a/b"}.lexically_relative("c:/c/d");    // ""
    fs::path{"C:/a/b"}.lexically_relative("D:/c/d");    // ""
    fs::path{"C:/a/b"}.lexically_proximate("D:/c/d");   // "C:/a/b"
\end{lstlisting}

\subsubsection{转换为字符串}
通过\texttt{u8string()}你可以将路径用作UTF-8字符串，
这是当前存储数据的通用格式。例如：
\begin{lstlisting}
    // 把路径存储为UTF-8字符串：
    std::vector<std::string> utf8paths; // 自从C++20起将改为std::u8string
    for (const auto& entry : fs::directory_iterator(p)) {
        utf8paths.push_back(entry.path().u8string());
    }
\end{lstlisting}
注意自从C++20起\texttt{u8string()}的返回值可能会从\texttt{std::string}改为
\texttt{std::u8string()}（新的UTF-8字符串类型和存储UTF-8字符
的\texttt{char8\_t}类型的提案见\url{https://wg21.link/p0482}）。
\footnote{感谢Tom Honermann指出这一点，并做出这个改进（C++开始提供真正的UTF-8支持是非常重要的）。}

成员模板\texttt{string<>()}可以用来转换成特殊的字符串类型，例如一个大小写无关的字符串类型：
\begin{lstlisting}
    struct ignoreCaseTraits : public std::char_traits<char> {
        // 大小写不敏感的比较两个字符：
        static bool eq(const char& c1, const char& c2) {
            return std::toupper(c1) == std::toupper(c2);
        }
        static bool lt(const char& c1, const char& c2) {
            return std::toupper(c1) < std::toupper(c2);
        }
        // 比较s1和s2的至多前n个字符：
        static int compare(const char* s1, const char* s2, std::size_t n);
        // 在s中搜索字符c：
        static const char* find(const char* s, std::size_t n, const char& c);
    };

    // 定义一个这种类型的字符串：
    using icstring = std::basic_string<char, ignoreCaseTraits>;

    std::filesystem::path p{"/dir\\subdir/subsubdir\\/./\\"};
    icstring s2 = p.string<char, ignoreCaseTraits>();
\end{lstlisting}
注意你\emph{不}应该使用函数\texttt{c\_str()}，因为它会转换为\emph{本地}字符串格式，
可能是\texttt{wchar\_t}，因此你需要使用\texttt{std::wcout}代替\texttt{std::cout}
来输出到输出流。

\subsection{本地和通用格式的转换}\label{ch20.3.4}
表\hyperref[t20.6]{本地和通用格式的转换}列出了在\hyperref[ch20.2.3]{通用路径格式}和
实际平台特定实现的格式之间转换的方法。
\begin{table}[htb]
    \centering
    \begin{tabular}{l|l}
        \hline
        \textbf{调用}                       & \textbf{效果}                                    \\
        \hline
        \texttt{p.generic\_string()}      & 返回\texttt{std::string}类型的通用路径                  \\
        \texttt{p.generic\_wstring()}     & 返回\texttt{std::wstring}类型的通用路径                 \\
        \texttt{p.generic\_u8string()}    & 返回\texttt{std::u8string}类型的通用路径                \\
        \texttt{p.generic\_u16string()}   & 返回\texttt{std::u16string}类型的通用路径               \\
        \texttt{p.generic\_u32string()}   & 返回\texttt{std::u32string}类型的通用路径               \\
        \texttt{p.generic\_string<...>()} & 返回\texttt{std::basic\_string<...>()}类型的通用路径    \\
        \texttt{p.native()}               & 返回\texttt{path::string\_type}类型的本地路径格式         \\
        \emph{到本地路径的转换}                   & 到本地字符串类型的隐式转换                                  \\
        \texttt{p.c\_str()}               & 返回本地字符串格式的字符序列形式的路径                            \\
        \texttt{p.make\_preferred()}      & 把\texttt{p}中的目录分隔符替换为本地格式的分隔符并返回修改后的\texttt{p} \\
        \hline
    \end{tabular}
    \caption{本地和通用格式的转换}
    \label{t20.6}
\end{table}

这些函数在POSIX兼容系统上没有效果，因为这些系统的本地格式和通用格式没有区别。
在其他平台上调用这些函数可能会有效果：
\begin{itemize}
    \item \texttt{generic...()}函数返回转换为\hyperref[ch20.2.3]{通用格式}之后的相应类型的字符串。
    \item \texttt{native()}返回用本地字符串编码的路径，其类型为\texttt{std::filesystem::path::string\_type}。
    这个类型在Windows下是\texttt{std::wstring}，这意味着你需要使用\texttt{std::wcout}
    代替\texttt{std::cout}来输出。新的重载允许我们向文件流传递本地字符串。
    \item \texttt{c\_str()}以空字符结尾的字符序列形式返回结果。注意使用这个函数是不可移植的，
    因为使用\texttt{std::cout}打印字符序列在Windows上的输出不正确。你应该使用\texttt{std::wcout}。
    \item \texttt{make\_preferred()}会使用本地的目录分隔符替换除了根名称之外的所有的目录分隔符。
    注意这是唯一一个会修改调用者的函数。因此，严格来讲，这个函数应该属于下一节的修改路径的函数，
    但因为它可以处理本地格式的转换，所以也在这里列出。
\end{itemize}
例如，在Windows上，下列代码：
\begin{lstlisting}
    std::filesystem::path p{"/dir\\subdir/subsubdir\\/./\\"};
    std::cout <<  "p:                  " << p << '\n';
    std::cout <<  "string():           " << p.string() << '\n';
    std::wcout << "wstring():          " << p.wstring() << '\n';
    std::cout <<  "lexically_normal(): " << p.lexically_normal() << '\n';
    std::cout <<  "generic_string():   " << p.generic_string() << '\n';
    std::wcout << "generic_wstring():  " << p.generic_wstring() << '\n';
    // 因为这是在Windows下，相应的本地字符串类型是wstring：
    std::wcout << "native():           " << p.native() << '\n'; // Windows!
    std::wcout << "c_str():            " << p.c_str() << '\n';
    std::cout <<  "make_preferred():   " << p.make_preferred() << '\n';
    std::cout <<  "p:                  " << p << '\n';
\end{lstlisting}
将会有如下输出：
\begin{blacklisting}
    p:                  "/dir\\subdir/subsubdir\\/./\\"
    string():           /dir\subdir/subsubdir\/./\
    wstring():          /dir\subdir/subsubdir\/./\
    lexically_normal(): "\\dir\\subdir\\subsubdir\\"
    generic_string():   /dir/subdir/subsubdir//.//
    generic_wstring():  /dir/subdir/subsubdir//.//
    native():           /dir\subdir/subsubdir\/./\
    c_str():            /dir\subdir/subsubdir\/./\
    make_preferred():   "\\dir\\subdir\\subsubdir\\\\.\\\\"
    p:                  "\\dir\\subdir\\subsubdir\\\\.\\\\"
\end{blacklisting}
再次注意：
\begin{itemize}
    \item 本地字符串格式是不可移植的。在Windows上是\texttt{wstring}，而在POSIX兼容系统上是\texttt{string}，
    这意味着你要使用\texttt{cout}而不是\texttt{wcout}来打印\texttt{native()}的结果。
    \item 只有\texttt{make\_preferred()}会修改调用者。其他的调用都会保持\texttt{p}不变。
\end{itemize}

\subsection{修改路径}\label{ch20.3.5}
表\hyperref[t20.7]{修改路径}列出了可以直接修改路径的操作。
\begin{table}[htb]
    \centering
    \begin{tabular}{l|l}
        \hline
        \textbf{调用}                         & \textbf{效果}                                            \\
        \hline
        \texttt{p = p2}                     & 赋予一个新路径                                                \\
        \texttt{p = sv}                     & 赋予一个字符串（视图）作为新路径                                       \\
        \texttt{p.assign(p2)}               & 赋予一个新路径                                                \\
        \texttt{p.assign(sv)}               & 赋予一个字符串（视图）作为新路径                                       \\
        \texttt{p.assign(beg, end)}         & 赋予从\texttt{beg}到\texttt{end}的元素组成的路径                   \\
        \texttt{p1 / p2}                    & 返回把\texttt{p2}作为子路径附加到\texttt{p1}之后的结果                 \\
        \texttt{p /= sub}                   & 把\texttt{sub}作为子路径附加到路径\texttt{p}之后                    \\
        \texttt{p.append(sub)}              & 把\texttt{sub}作为子路径附加到路径\texttt{p}之后                    \\
        \texttt{p.append(beg, end)}         & 把从\texttt{beg}到\texttt{end}之间的元素作为子路径附加到路径\texttt{p}之后 \\
        \texttt{p += str}                   & 把\texttt{str}里的字符添加到路径\texttt{p}之后                     \\
        \texttt{p.concat(str)}              & 把\texttt{str}里的字符添加到路径\texttt{p}之后                     \\
        \texttt{p.concat(beg, end)}         & 把从\texttt{beg}到\texttt{end}之间的元素附加到路径\texttt{p}之后      \\
        \texttt{p.remove\_filename()}       & 移除路径末尾的文件名                                             \\
        \texttt{p.replace\_filename(repl)}  & 替换末尾的文件名（如果有的话）                                        \\
        \texttt{p.replace\_extension()}     & 移除末尾的文件的扩展名                                            \\
        \texttt{p.replace\_extension(repl)} & 替换末尾的文件的扩展名（如果有的话）                                     \\
        \texttt{p.clear()}                  & 清空路径                                                   \\
        \texttt{p.swap(p2)}                 & 交换两个路径                                                 \\
        \texttt{swap(p1, p2)}               & 交换两个路径                                                 \\
        \texttt{p.make\_preferred()}        & 把\texttt{p}中的目录分隔符替换为本地格式的分隔符并返回修改后的\texttt{p}         \\
        \hline
    \end{tabular}
    \caption{修改路径}
    \label{t20.7}
\end{table}

注意\texttt{+=}和\texttt{concat()}简单的把字符添加到路径后，\texttt{/}、\texttt{/=}、
\texttt{append()}则是在路径后用目录分隔符添加一个子路径：
\begin{lstlisting}
    std::filesystem::path p{"myfile"};
    p += ".git";        // p:myfile.git
    p /= ".git";        // p:myfile.git/.git
    p.concat("1");      // p:myfile.git/.git1
    p.append("1");      // P:myfile.git/.git1/1
    std::cout << p << '\n';
    std::cout << p / p << '\n';
\end{lstlisting}
在POSIX兼容系统上输出将是：
\begin{blacklisting}
    "myfile.git/.git1/1"
    "myfile.git/.git1/1/myfile.git/.git1/1"
\end{blacklisting}
在Windows系统上输出将是：
\begin{blacklisting}
    "myfile.git\\.git1\\1"
    "myfile.git\\.git1\\1\\myfile.git\\.git1\\1"
\end{blacklisting}
注意如果添加一个绝对路径子路径意味着替换原本的路径。例如，如下操作之后：
\begin{lstlisting}
    namespace fs = std::filesystem;
    auto p1 = fs::path("/usr") / "tmp";     // 路径是/usr/tmp或者/usr\tmp
    auto p2 = fs::path("/usr/") / "tmp";    // 路径是/usr/tmp
    auto p3 = fs::path("/usr") / "/tmp";    // 路径是/tmp
    auto p4 = fs::path("/usr/") / "/tmp";   // 路径是/tmp
\end{lstlisting}
我们有了四个指向两个不同文件的路径：
\begin{itemize}
    \item \texttt{p1}和\texttt{p2}相等，都指向文件\texttt{/usr/tmp}（注意在Windows上
    它们相等，但\texttt{p1}将是\texttt{/usr\textbackslash tmp}）。
    \item \texttt{p3}和\texttt{p4}相等，指向文件\texttt{/tmp}，因为附加的子路径是绝对路径。
\end{itemize}
对于根元素，是否赋予新的根元素的结果将不同。例如，在Windows上，结果将是：
\begin{lstlisting}
    auto p1 = fs::path("usr") / "C:/tmp";   // 路径是C:/tmp
    auto p2 = fs::path("usr") / "C:";       // 路径是C:
    auto p3 = fs::path("C:") / "":          // 路径是C:
    auto p4 = fs::path("C:usr") / "/tmp";   // 路径是C:/tmp
    auto p5 = fs::path("C:usr") / "C:tmp";  // 路径是C:usr\tmp
    auto p6 = fs::path("C:usr") / "c:tmp";  // 路径是c:tmp
    auto p7 = fs::path("C:usr") / "D:tmp";  // 路径是D:tmp
\end{lstlisting}
函数\texttt{make\_preferred()}把一个路径内的目录分隔符替换成本地格式。
例如：
\begin{lstlisting}
    std::filesystem::path p{"//server/dir//subdir///file.txt"};
    p.make_preferred();
    std::cout << p << '\n';
\end{lstlisting}
在POSIX兼容系统上输出将是：
\begin{blacklisting}
    "//server/dir/subdir/file.txt"
\end{blacklisting}
在Windows系统上输出将是：
\begin{blacklisting}
    "\\\\server\\dir\\\\subdir\\\\\\file.txt"
\end{blacklisting}
注意开头的根名称没有被修改，因为它由两个斜杠或者反斜杠组成。
也注意这个函数在POSIX兼容系统上也不会把反斜杠转换成斜杠，因为反斜杠不被识别为目录分隔符。

\texttt{replace\_extension()}可以替换、添加、或者删除扩展名：
\begin{itemize}
    \item 如果文件已经有扩展名了，它会进行替换。
    \item 如果文件没有扩展名，会添加新的扩展名。
    \item 如果你跳过了新的扩展名参数或者新扩展名参数为空，它会移除已有的扩展名。
\end{itemize}
替换时是否有前导的点没有影响。函数会确保文件名和扩展名之间有且只有一个点。例如：
\begin{lstlisting}
    fs::path{"file.txt"}.replace_extension("tmp")   // file.tmp
    fs::path{"file.txt"}.replace_extension(".tmp")  // file.tmp
    fs::path{"file.txt"}.replace_extension("")      // file
    fs::path{"file.txt"}.replace_extension()        // file
    fs::path{"dir"}.replace_extension("tmp")        // dir.tmp
    fs::path{".git"}.replace_extension("tmp")       // .git.tmp
\end{lstlisting}
注意“纯扩展名”的文件名（例如\texttt{.git}）不会被当作扩展名。
\footnote{自从C++17起才改成这样。在C++17之前，最后一条语句的结果将是\texttt{.tmp}。}

\subsection{比较路径}\label{ch20.3.6}
表\hyperref[t20.8]{比较路径}列出了可以比较两个路径的操作。
\begin{table}[htb]
    \centering
    \begin{tabular}{l|l}
        \hline
        \textbf{调用}                 & \textbf{效果}                                    \\
        \hline
        \texttt{p1 == p2}           & 返回两个路径是否相等                                     \\
        \texttt{p1 != p2}           & 返回两个路径是否不相等                                    \\
        \texttt{p1 < p2}            & 返回一个路径是否小于另一个                                  \\
        \texttt{p1 <= p2}           & 返回一个路径是否小于等于另一个                                \\
        \texttt{p1 >= p2}           & 返回一个路径是否大于等于另一个                                \\
        \texttt{p1 > p2}            & 返回一个路径是否大于另一个                                  \\
        \texttt{p.compare(p2)}      & 返回\texttt{p}是小于、等于还是大于\texttt{p2}              \\
        \texttt{p.compare(sv)}      & 返回\texttt{p}是小于、等于还是大于字符串（视图）\texttt{sv}转换成的路径 \\
        \texttt{equivalent(p1, p2)} & 访问实际文件系统的开销较大的比较操作                             \\
        \hline
    \end{tabular}
    \caption{比较路径}
    \label{t20.8}
\end{table}

注意这些比较操作大部分都不会访问实际的文件系统，这意味着它们只是以词法的方式比较，
这样开销很小但可能会导致令人惊奇的结果：
\begin{itemize}
    \item 使用\texttt{==}、\texttt{!=}、\texttt{compare()}的结果是下面的路径都不相同：
    \begin{blacklisting}
    tmp1/f
    ./tmp1/f
    tmp1/./f
    tmp1/tmp11/../f
    \end{blacklisting}
    \item 只有当分隔符以外的部分都相同时才会判定为相同。
    因此，下面的路径是相等的（假设反斜杠也是有效的目录分隔符）：
    \begin{blacklisting}
    tmp1/f
    tmp1//f
    tmp1\f
    tmp1/\/f
    \end{blacklisting}
\end{itemize}
如果你使用了\texttt{lexically\_normal()}那么上面的所有路径都相等
（假设反斜杠也是有效的目录分隔符）。例如：
\begin{lstlisting}
    std::filesystem::path p1{"tmp1/f"};
    std::filesystem::path p2{"./tmp1/f"};

    p1 == p2                                                // false
    p1.compare(p2)                                          // 非0
    p1.lexically_normal() == p2.lexically_normal()          // true
    p1.lexically_normal().compare(p2.lexically_normal())    // 0
\end{lstlisting}
如果你想要访问实际的文件系统来正确处理包含符号链接的情况，你需要使用\texttt{equivalent()}。
然而，注意这个函数要求两个路径都代表已经存在的文件。
因此，一个通用的尽可能精确（但没有最佳的性能）的比较两个路径的方法是：
\begin{lstlisting}
    bool pathsAreEqual(const std::filesystem::path& p1,
                       const std::filesystem::path& p2)
    {
        return exists(p1) && exists(p2) ? equivalent(p1, p2)
            : p1.lexically_normal() == p2.lexically_normal();
    }
\end{lstlisting}

\subsection{其他路径操作}
表\hyperref[t20.9]{其他路径操作}列出了剩余的路径操作。
\begin{table}[htb]
    \centering
    \begin{tabular}{l|l}
        \hline
        \textbf{调用}              & \textbf{效果} \\
        \hline
        \texttt{p.hash\_value()} & 返回一个路径的哈希值  \\
        \hline
    \end{tabular}
    \caption{其他路径操作}
    \label{t20.9}
\end{table}

注意只有\hyperref[ch20.3.6]{相等的路径}才保证有相同的哈希值，下面的路径返回不同的哈希值：
\begin{blacklisting}
    tmp1/f
    ./tmp1/f
    tmp1/./f
    tmp1/tmp11/../f
\end{blacklisting}
因此，在把它们放入哈希表之前，你可能需要先对路径进行正规化。


\section{文件系统操作}
这一节介绍开销更大的会访问实际文件系统的操作。

因为这些操作通常会访问文件系统（要确定文件是否存在、解析符号链接等等），
它们比纯路径操作的开销要大的多。因此，它们通常是独立函数。

\subsection{文件属性}\label{ch20.4.1}
对于一个路径你可以查询很多文件属性。首先，表\hyperref[t20.10]{文件类型操作}列出了
可以检查路径\texttt{p}是否指向特定的文件并查询它的类型（如果有的话）的操作。
注意这些操作都会访问实际的文件系统，也都是独立函数。
\begin{table}[htb]
    \centering
    \begin{tabular}{l|l}
        \hline
        \textbf{调用}                     & \textbf{效果}                        \\
        \hline
        \texttt{exists(p)}              & 返回是否存在一个可访问到的文件                    \\
        \texttt{is\_symlink(p)}         & 返回是否文件\texttt{p}存在并且是符号链接          \\
        \texttt{is\_regular\_file(p)}   & 返回是否文件\texttt{p}存在并且是普通文件          \\
        \texttt{is\_directory(p)}       & 返回是否文件\texttt{p}存在并且是目录            \\
        \texttt{is\_other(p)}           & 返回是否文件\texttt{p}存在并且不是普通文件或目录或符号链接 \\
        \texttt{is\_block\_file(p)}     & 返回是否文件\texttt{p}存在并且是块特殊文件         \\
        \texttt{is\_character\_file(p)} & 返回是否文件\texttt{p}存在并且是字符特殊文件        \\
        \texttt{is\_fifo(p)}            & 返回是否文件\texttt{p}存在并且是FIFO或者管道文件    \\
        \texttt{is\_socket(p)}          & 返回是否文件\texttt{p}存在并且是套接字           \\
        \hline
    \end{tabular}
    \caption{文件类型操作}
    \label{t20.10}
\end{table}

文件系统类型的函数和相应的\hyperref[ch20.2.7]{\texttt{file\_type}值}相对应。
然而，注意这些函数（除了\texttt{is\_symlink()}之外）都会解析符号链接。也就是说，
对于一个指向目录的符号链接，\texttt{is\_symlink()}和\texttt{is\_directory()}都返回\texttt{true}。

还要注意对于\hyperref[文件类型]{特殊文件}（非普通文件、非目录、非符号链接）的检查，
根据\hyperref[文件类型]{其他文件类型}的定义，\texttt{is\_other()}会返回\texttt{true}。

对于实现特定的文件类型没有明确的便捷函数，这意味着只有\texttt{is\_other()}会返回
\texttt{true}（如果是一个指向这种文件的符号链接，那么\texttt{is\_symlink()}也会返回\texttt{true}）。
你可以使用\hyperref[ch20.4.2]{文件状态API}来检测这些特定的类型。

如果不想解析符号链接，可以像下面将要讨论的\texttt{exists()}一样使用\hyperref[t20.12]{\texttt{symlink\_status()}}，
并对返回的\hyperref[ch20.4.2]{\texttt{file\_status}}调用这些函数。

\subsubsection{检查文件是否存在}\label{ch20.4.1.1}
\texttt{exists()}检查是否有一个可以打开的文件。像之前讨论的一样，它会解析符号链接。
因此，对于指向不存在文件的符号链接它会返回\texttt{false}。

因此，下面的代码不能像预期中工作：
\begin{lstlisting}
    // 如果不存在文件p，创建一个符号链接p指向file：
    if (!exists(p)) {   // OOPS：检查文件p指向的目标是否不存在
        std::filesystem::create_symlink(file, p);
    }
\end{lstlisting}
如果\texttt{p}已经存在并且是一个指向不存在文件的符号链接，
\texttt{create\_symlink()}将会尝试在\texttt{p}位置处创建新的符号链接，
这会导致抛出一个相应的异常。

因为多用户/多进程操作系统中文件系统的状况可能会在任意时刻改变，最佳的方法就是尝试进行操作
并在失败时处理错误。因此，我们可以直接进行操作并\hyperref[ch20.2.6.1]{处理相应的异常}或者
传递额外的参数来\hyperref[ch20.2.6.2]{处理错误码}。

然而，有时你必须检测文件是否存在（在进行文件系统操作之前）。
例如，如果你想在指定位置创建一个文件并且该位置处已经有了一个符号链接，
那么新创建的文件（可能）会在一个意料之外的地方创建或者覆盖。
这种情况下，你应该像下面这样检查文件是否存在：
\footnote{感谢Billy O’Neal指出这一点。}
\begin{lstlisting}
    if (!exists(symlink_status(p))) {   // OK：检查p是否还不存在（作为符号链接）
        ...
    }
\end{lstlisting}
这里，我们使用了\hyperref[t20.12]{\texttt{symlink\_status()}}，
它会在\emph{不}解析符号链接的情况下返回文件状态，以检查位置\texttt{p}处是否有任何文件存在。

\subsubsection{其他文件属性}
表\hyperref[t20.11]{文件属性操作}列出了一些检查额外文件属性的独立函数。
\begin{table}[htb]
    \centering
    \begin{tabular}{l|l}
        \hline
        \textbf{调用}                   & \textbf{效果}   \\
        \hline
        \texttt{is\_empty()}          & 返回文件是否为空      \\
        \texttt{file\_size()}         & 返回文件大小        \\
        \texttt{hard\_link\_count(p)} & 返回硬链接数量       \\
        \texttt{last\_write\_time(p)} & 返回最后一次修改文件的时间 \\
        \hline
    \end{tabular}
    \caption{文件属性操作}
    \label{t20.11}
\end{table}

注意路径是否为空和路径指向的文件是否为空是不同的：
\begin{lstlisting}
    p.empty()       // 如果路径p为空则返回true（开销很小）
    is_empty(p)     // 如果路径p处的文件为空返回true（文件系统操作）
\end{lstlisting}
如果文件存在并且是普通文件，那么\texttt{file\_size(p)}以字节为单位返回文件\texttt{p}的大小
（在POSIX系统上，这个值和\texttt{stat()}返回的\texttt{st\_size}成员的值相同）。
对于所有其他文件，结果是实现特定的，因此不可移植。

\texttt{hard\_link\_count(p)}返回文件系统中某一个文件存在的次数。
通常情况下，这个数字是1，但是在某些文件系统上同一个文件可以出现在不同的位置（也就是有多个不同的路径）。
这和软链接指向其他文件不同，这里的路径可以直接访问文件。只有当最后一个硬链接被删除时文件才会被删除。

\subsubsection{处理最后修改的时间}\label{ch20.4.1.3}
\texttt{last\_write\_time(p)}返回文件最后一次修改或者写入的时间。
返回的类型是标准\texttt{chrono}库里的时间点类型\texttt{time\_point}：
\begin{lstlisting}
    namespace std::filesystem {
        using file_time_type = chrono::time_point<travialClock>;
    }
\end{lstlisting}
时钟类型\emph{trivialClock}是一个实现特定的时钟类型，它能反应时钟的精度和范围。
例如，你可以这么使用它：
\begin{lstlisting}
    void printFileTime(const std::filesystem::path& p)
    {
        auto filetime = last_write_time(p);
        auto diff = std::filesystem::file_time_type::clock::now() - filetime;
        std::cout << p << " is "
                  << std::chrono::duration_cast<std::chrono::seconds>(diff).count()
                  << " Seconds old.\n";
    }
\end{lstlisting}
输出可能是：
\begin{blacklisting}
    "fileattr.cpp" is 4 Seconds old.
\end{blacklisting}
这个例子中，你可以使用：
\begin{lstlisting}
    decltype(filetime)::clock::now()
\end{lstlisting}
来代替
\begin{lstlisting}
    std::filesystem::file_time_type::clock::now()
\end{lstlisting}
注意文件系统时间点使用的时钟不保证是标准的\texttt{system\_clock}。
因此，现在还没有把文件系统时间点转换为\texttt{time\_t}然后在字符串或者输出中用作绝对时间的标准化支持。
\footnote{C++20中引入的\texttt{clock\_cast}将修复这个问题。}
然而还是有一些解决方法的，下面的代码“粗略地”把任何时钟的时间点转换为\texttt{time\_t}对象：
\begin{lstlisting}
    template<typename TimePoint>
    std::time_t toTimeT(TimePoint tp)
    {
        using system_clock = std::chrono::system_clock;
        return system_clock::to_time_t(system_clock::now() + (tp - decltype(tp)::clock::now()));
    }
\end{lstlisting}
技巧是计算出文件系统时间点和现在的差值，类型是\texttt{duration}，然后加上现在的系统时钟的时间
就可以得到用系统时钟表示的文件系统时间点。这个函数不是很精确，因为不同的时钟可能有不同的精度，
而且两次\texttt{now()}调用之间可能时间差。然而，一般来说，这个函数工作得很好。

例如，对一个路径\texttt{p}我们可以调用：
\begin{lstlisting}
    auto ftime = last_write_time(p);
    std::time_t t = toTimeT(ftime);
    // 转换为日历时间（跳过末尾的换行符）：
    std::string ts = ctime(&t);
    ts.resize(ts.size()-1);
    std::cout << "last access of " << p << ": " << ts << '\n';
\end{lstlisting}
输出可能是：
\begin{blacklisting}
    last access of "fileattr.exe": Sun Jun 24 10:41:12 2018
\end{blacklisting}
为了把时间格式化为我们想要的格式，我们可以这样调用：
\begin{lstlisting}
    std::time_t t = toTimeT(ftime);
    char mbstr[100];
    if (std::strftime(mbstr, sizeof(mbstr), "last access: %B %d, %Y at %H:%M\n",
                      std::localtime(&t))) {
        std::cout << mbstr;
    }
\end{lstlisting}
输出可能是：
\begin{blacklisting}
    last access: June 24, 2018 at 10:41
\end{blacklisting}
把任意文件系统时间点转换为字符串的一个有用的辅助函数可以是：
\inputcodefile{filesystem/ftimeAsString.hpp}
注意\texttt{ctime()}和\texttt{strftime()}是非线程安全的，不能在并发环境中使用。

参见\nameref{ch20.4.4.2}小节查看相应的修改最后访问时间的API。

\subsection{文件状态}\label{ch20.4.2}
为了避免文件系统访问，有一个特殊的类型\texttt{file\_status}可以被用来存储并修改
被缓存的文件类型和权限。发生以下情况时可以设置状态：
\begin{itemize}
    \item 当使用表\hyperref[t20.12]{文件状态的操作}中的方法访问某个指定路径的文件状态时
    \item 当\nameref{ch20.5}时
\end{itemize}
\begin{table}[htb]
    \centering
    \begin{tabular}{l|l}
        \hline
        \textbf{调用}                 & \textbf{效果}                                   \\
        \hline
        \texttt{status(p)}          & 返回文件\texttt{p}的\texttt{file\_status}（解析符号链接）  \\
        \texttt{symlink\_status(p)} & 返回文件\texttt{p}的\texttt{file\_status}（不解析符号链接） \\
        \hline
    \end{tabular}
    \caption{文件状态的操作}
    \label{t20.12}
\end{table}
不同之处在于路径\texttt{p}是否解析符号链接，\texttt{status()}将会解析符号链接
并返回指向的文件的属性（文件状态也可能是没有文件），而\texttt{symlink\_status(p)}
将会返回符号链接自身的状态。

表\hyperref[t20.13]{\texttt{file\_status}的操作}列出了\texttt{file\_status}类型的对象\texttt{fs}的所有操作。
\begin{table}[htb]
    \centering
    \begin{tabular}{l|l}
        \hline
        \textbf{调用}                      & \textbf{效果}               \\
        \hline
        \texttt{exists(fs)}              & 返回是否有文件存在                 \\
        \texttt{is\_regular\_file(fs)}   & 返回是否有文件存在并且是普通文件          \\
        \texttt{is\_directory(fs)}       & 返回是否有文件存在并且是目录            \\
        \texttt{is\_symlink(fs)}         & 返回是否有文件存在并且是符号链接          \\
        \texttt{is\_other(fs)}           & 返回是否有文件存在并且不是普通文件或目录或符号链接 \\
        \texttt{is\_character\_file(fs)} & 返回是否有文件存在并且是字符特殊文件        \\
        \texttt{is\_block\_file(fs)}     & 返回是否有文件存在并且是块特殊文件         \\
        \texttt{is\_fifo(fs)}            & 返回是否有文件存在并且是FIFO或者管道文件    \\
        \texttt{is\_socket(fs)}          & 返回是否有文件存在并且是套接字           \\
        \texttt{fs.type()}               & 返回文件的\texttt{file\_type}  \\
        \texttt{fs.permissions()}        & 返回文件的\nameref{ch20.4.3}   \\
        \hline
    \end{tabular}
    \caption{\texttt{file\_status}的操作}
    \label{t20.13}
\end{table}

使用状态操作的一个好处是可以节省同一个文件的多次操作系统调用。例如，
原本的如下代码：
\begin{lstlisting}
    if (!is_directory(path)) {
        if (is_character_file(path) || is_block_file(path)) {
            ...
        }
        ...
    }
\end{lstlisting}
可以用如下方式更高效地实现：
\begin{lstlisting}
    auto pathStatus{status(path)};
    if (!is_directory(pathStatus)) {
        if (is_character_file(pathStatus) || is_block_file(pathStatus)) {
            ...
        }
        ...
    }
\end{lstlisting}
另一个关键的好处是通过使用\texttt{symlink\_status()}，你可以在\emph{不解析}任何符号链接的
情况下检查文件状态。这很有用，例如当你想检测指定路径处\hyperref[ch20.4.1.1]{是否有文件存在}时。

因为文件状态不使用操作系统，所以没有提供相应的返回错误码的版本。

以\hyperref[ch20.4.1]{路径作为参数}的\texttt{exists()}和\texttt{is\_...()}函数是
获取并检查文件状态的\texttt{type()}的缩写。例如，
\begin{lstlisting}
    is_regular_file(mypath)
\end{lstlisting}
是如下代码的缩写：
\begin{lstlisting}
    is_regular_file(status(mypath))
\end{lstlisting}
上面的代码又是下面代码的缩写：
\begin{lstlisting}
    status(mypath).type() == file_type::regular
\end{lstlisting}

\subsection{权限}\label{ch20.4.3}
处理权限的模型来自于UNIX/POSIX的世界。用若干位来表示文件所有者、同组其他用户、其他用户的
读、写、执行/搜索的权限。另外，还有特殊的位表示“运行时设置用户ID”、“运行时设置组ID”
和粘贴位（或者其他操作系统特定的含义）。

表\hyperref[t20.14]{权限位}列出了位域枚举类型\texttt{perms}的值，该类型定义在
\texttt{std::filesystem}中，表示一个或多个权限位。
\begin{table}[htb]
    \centering
    \begin{tabular}{l|r|l|l}
        \hline
        \textbf{枚举}            & \textbf{8进制值}   & \textbf{POSIX}    & \textbf{含义}  \\
        \hline
        \texttt{none}          & \texttt{0}      &                   & 没有权限集        \\
        \texttt{owner\_read}   & \texttt{0400}   & \texttt{S\_IRUSR} & 所属用户有读权限     \\
        \texttt{owner\_write}  & \texttt{0200}   & \texttt{S\_IWUSR} & 所属用户有写权限     \\
        \texttt{owner\_exec}   & \texttt{0100}   & \texttt{S\_IXUSR} & 所属用户有执行/搜索权限 \\
        \texttt{owner\_all}    & \texttt{0700}   & \texttt{S\_IRWXU} & 所属用户有所有权限    \\
        \texttt{group\_read}   & \texttt{040}    & \texttt{S\_IRGRP} & 同组用户有读权限     \\
        \texttt{group\_write}  & \texttt{020}    & \texttt{S\_IWGRP} & 同组用户有写权限     \\
        \texttt{group\_exec}   & \texttt{010}    & \texttt{S\_IXGRP} & 同组用户有执行/搜索权限 \\
        \texttt{group\_all}    & \texttt{070}    & \texttt{S\_IRWXG} & 同组用户有所有权限    \\
        \texttt{others\_read}  & \texttt{04}     & \texttt{S\_IROTH} & 其他用户有读权限     \\
        \texttt{others\_write} & \texttt{02}     & \texttt{S\_IWOTH} & 其他用户有写权限     \\
        \texttt{others\_exec}  & \texttt{01}     & \texttt{S\_IXOTH} & 其他用户有执行/搜索权限 \\
        \texttt{others\_all}   & \texttt{07}     & \texttt{S\_IRWXO} & 其他用户有所有权限    \\
        \texttt{all}           & \texttt{0777}   &                   & 所有用户有所有权限    \\
        \texttt{set\_uid}      & \texttt{04000}  & \texttt{S\_ISUID} & 运行时设置用户ID    \\
        \texttt{set\_gid}      & \texttt{02000}  & \texttt{S\_ISGID} & 运行时设置组ID     \\
        \texttt{sticky\_bit}   & \texttt{01000}  & \texttt{S\_ISVTX} & 操作系统特定       \\
        \texttt{mask}          & \texttt{07777}  &                   & 所有可能位的掩码     \\
        \texttt{unknown}       & \texttt{0xFFFF} &                   & 未知权限         \\
        \hline
    \end{tabular}
    \caption{权限位}
    \label{t20.14}
\end{table}

你可以查询当前的权限并检查返回的\texttt{perms}对象。为了组合标记，你需要使用位运算。例如：
\begin{lstlisting}
    // 如果可写：
    if ((fileStatus.permissions() &
        (fs::perms::owner_write | fs::perms::group_write | fs::perms::others_write))
        != fs::perms::none) {
        ...
    }
\end{lstlisting}
一个较短（但可读性较差）的初始化位掩码的方法是直接使用相应的8进制值
和\hyperref[ch8.3]{更宽松的枚举初始化}特性：
\begin{lstlisting}
    // 如果可写：
    if ((fileStatus.permissions() & fs::perms{0222}) != fs::perms::none) {
        ...
    }
\end{lstlisting}
注意你必须把\texttt{\&}表达式放在括号里，因为它的优先级低于比较运算符。
还要注意不能跳过比较，因为没有从位域枚举类型到\texttt{bool}的隐式类型转换。

作为另一个例子，为了像UNIX命令\texttt{ls -l}一样把权限位转换为字符串，你可以使用
如下的辅助函数：
\inputcodefile{filesystem/permAsString.hpp}
这允许你使用标准输出打印出文件的权限：
\begin{lstlisting}
    std::cout << "permissions: " << asString(stasus(mypath).permissions()) << '\n';
\end{lstlisting}
对于一个所有者拥有所有权限、其他用户拥有读和执行权限的文件，输出可能是：
\begin{blacklisting}
    permissions: rwxr-xr-x
\end{blacklisting}
然而，注意Windows的ACL（访问控制列表）并不是完全按照这套方案来划分权限的。
因此，当使用Visual C++时，可写的文件\emph{总是}同时有读、写、执行位被设置
（即使它\emph{不是}可执行文件），带有只写标志的文件通常有读和执行位设置。
这也影响了\hyperref[可移植的修改权限]{可移植地修改权限}的API。\label{ACL}

\subsection{修改文件系统}
你可以通过创建、删除或者修改已有文件来修改文件系统。

\subsubsection{创建和删除文件}
表\hyperref[t20.15]{创建和删除文件}列出了通过路径\texttt{p}创建和删除文件的操作。
\begin{table}[htb]
    \centering
    \begin{tabular}{l|l}
        \hline
        \textbf{调用}                                  & \textbf{效果}                              \\
        \hline
        \texttt{create\_directory(p)}                & 创建目录                                     \\
        \texttt{create\_directory(p, attrPath)}      & 创建属性为\texttt{attrPath}的目录                \\
        \texttt{create\_directories(p)}              & 创建目录和该路径中所有不存在的目录                        \\
        \texttt{create\_hard\_link(to, new)}         & 为已存在文件\texttt{to}创建另一个文件系统项\texttt{new}  \\
        \texttt{create\_symlink(to, new)}            & 创建指向\texttt{to}的符号链接\texttt{new}         \\
        \texttt{create\_directory\_symlink(to, new)} & 创建指向目录\texttt{to}的符号链接\texttt{new}       \\
        \texttt{copy(from, to)}                      & 拷贝任意类型的文件                                \\
        \texttt{copy(from, to, options)}             & 以选项\texttt{options}拷贝任意类型的文件             \\
        \texttt{copy\_file(from, to)}                & 拷贝文件（不能是目录或者符号链接）                        \\
        \texttt{copy\_file(from, to, options)}       & 以选项\texttt{options}拷贝文件                  \\
        \texttt{copy\_symlink(from, to)}             & 拷贝符号链接（\texttt{to}也指向\texttt{from}指向的文件） \\
        \texttt{remove(p)}                           & 删除一个文件或者空目录                              \\
        \texttt{remove\_all(p)}                      & 删除路径\texttt{p}并递归删除所有子文件（如果有的话）          \\
        \hline
    \end{tabular}
    \caption{创建和删除文件}
    \label{t20.15}
\end{table}

没有创建普通文件的函数。这可以通过标准I/O流库做到。例如，下面的语句会创建一个新的空文件
（如果不存在的话）：
\begin{lstlisting}
    std::ofstream{"log.txt"};
\end{lstlisting}
创建一个或多个目录的函数返回指定目录是否被创建。
因此，如果指定目录已经存在的话并不会返回错误。
\footnote{一开始，C++17还指明如果已经有非目录的文件存在时也不返回错误。
之后这被作为C++17的缺陷修复了（见\url{https://wg21.link/p1164r1}）。}

\texttt{copy...()}函数对\hyperref[文件类型]{特殊文件类型}无效。默认情况下它们会：
\begin{itemize}
    \item 如果已经存在文件时报错
    \item 不递归操作
    \item 解析符号链接
\end{itemize}
这些默认行为可以通过参数\texttt{options}覆盖，该参数的类型是位域枚举类型\texttt{copy\_options}，
定义在命名空间\texttt{std::filesystem}中。
表\hyperref[t20.16]{拷贝选项}列出了所有可能的值。
\begin{table}[htb]
    \centering
    \begin{tabular}{l|p{0.7\textwidth}}
        \hline
        \texttt{copy\_options}       & \textbf{效果}                            \\
        \hline
        \texttt{none}                & 默认值（值为0）                               \\
        \texttt{skip\_existing}      & 跳过覆盖已有文件                               \\
        \texttt{overwrite\_existing} & 覆盖已有文件                                 \\
        \texttt{update\_existing}    & 如果新文件更新的话覆盖已有文件                        \\
        \texttt{recursive}           & 递归拷贝子目录和内容                             \\
        \texttt{copy\_symlinks}      & 拷贝符号链接为符号链接                            \\
        \texttt{skip\_symlinks}      & 忽略符号链接                                 \\
        \texttt{directories\_only}   & 只拷贝目录                                  \\
        \texttt{create\_hard\_links} & 创建新的硬链接而不是拷贝文件                         \\
        \texttt{create\_symlinks}    & 创建符号链接而不是拷贝文件（源路径必须是绝对路径，除非目标路径在当前目录下） \\
        \hline
    \end{tabular}
    \caption{拷贝选项}
    \label{t20.16}
\end{table}

当创建目录的符号链接时，使用\texttt{create\_directory\_symlink()}
比\texttt{create\_symlink()}更好，因为有些操作系统需要显式指明目标是一个目录。
注意第一个参数是相对于要创建的符号链接所在的目录的相对路径。
因此，要创建一个指向\texttt{sub/file.txt}的符号链接\texttt{sub/slink}，你必须调用：
\begin{lstlisting}
    std::filesystem::create_symlink("file.txt", "sub/slink");
\end{lstlisting}
语句
\begin{lstlisting}
    std::filesystem::create_symlink("sub/file.txt", "sub/slink");
\end{lstlisting}
将会创建一个指向\texttt{sub/sub/file.txt}的符号链接\texttt{sub/slink}。

删除文件的函数有以下行为：
\begin{itemize}
    \item \texttt{remove()}删除一个文件或者空目录。
    如果没有要删除的文件/目录或者不能被删除时返回\texttt{false}而不会抛出异常。
    \item \texttt{remove\_all()}递归删除一个文件或者目录。它返回一个\texttt{uintmax\_t}
    类型的值表示删除的文件数量。如果没有文件时返回\texttt{0}，如果出错时返回\texttt{uintmax\_t(-1)}
    而不会抛出异常。
\end{itemize}
在这两种情况下，都会删除符号链接本身而不是它们指向的文件。

注意当传递字符串字面量作为参数时你应该总是正确地使用完全限定的\texttt{remove()}，
否则可能会\hyperref[ADL导致意外行为]{调用C函数\texttt{remove()}}。

\subsubsection{修改已存在的文件}\label{ch20.4.4.2}
表\hyperref[t20.17]{修改文件}列出了修改已存在文件的操作。
\begin{table}[htb]
    \centering
    \begin{tabular}{l|l}
        \hline
        \textbf{调用}                            & \textbf{效果}           \\
        \hline
        \texttt{rename(old, new)}              & 重命名并/或移动文件            \\
        \texttt{last\_write\_time(p, newtime)} & 修改最后修改时间              \\
        \texttt{permissions(p, prms)}          & 用\texttt{prms}替换文件权限  \\
        \texttt{permissions(p, prms, mode)}    & 根据\texttt{mode}修改文件权限 \\
        \texttt{resize\_file(p, newSize)}      & 修改普通文件的大小             \\
        \hline
    \end{tabular}
    \caption{修改文件}
    \label{t20.17}
\end{table}

\texttt{rename()}可以处理任何类型的文件（包括目录和符号链接）。对于符号链接，它会重命名符号链接本身，
而不是指向的文件。注意\texttt{rename()}需要完整的包含文件名的目标路径：
\begin{lstlisting}
    // 移动"tmp/sub/x"到"tmp/x":
    std::filesystem::rename("tmp/sub/x", "tmp");    // ERROR
    std::filesystem::rename("tmp/sub/x", "tmp/x");  // OK
\end{lstlisting}

\texttt{last\_write\_time()}使用在“\nameref{ch20.4.1.3}”小节介绍的时间点格式。例如：
\begin{lstlisting}
    // 创建文件p（更新最后修改时间）：
    last_write_time(p, std::filesystem::file_time_type::clock::now());
\end{lstlisting}

\texttt{permissions()}使用了在“\nameref{ch20.4.3}”小节描述的API。可选的\texttt{mode}
参数是一个位域枚举类型，定义在命名空间\texttt{std::filesystem}中。一方面，它允许你在
\texttt{replace}、\texttt{add}、\texttt{remove}之间选择。另一方面，使用\texttt{nofollow}
可以让你修改符号链接本身的权限而不是它们指向的文件的权限。
例如：
\begin{lstlisting}
    // 移除同组用户的写权限和其他用户的所有权限：
    permissions(mypath, std::filesystem::perms::group_write | std::filesystem::perms::others_all,
                        std::filesystem::perm_options::remove);
\end{lstlisting}
再次注意Windows支持的权限概念有些不同。它的\hyperref[ACL]{ACL权限概念}只支持两种方式：\label{可移植的修改权限}
\begin{itemize}
    \item 所有用户可读、写、执行/搜索（\texttt{rwxrwxrwx}）
    \item 所有用户可读、执行/搜索（\texttt{r-xr-xr-x}）
\end{itemize}
为了在两种模式之间可移植地进行切换，你必须同时启用或者禁用三个写权限
（只删除一个没有用）：
\begin{lstlisting}
    // 可移植地启用/禁用写权限选项值：
    auto allWrite = std::filesystem::perms::owner_write
                    | std::filesystem::perms::group_write
                    | std::filesystem::perms::others_write;
    // 可移植地移除写权限：
    permissions(file, allWrite, std::filesystem::perm_options::remove);
\end{lstlisting}
一个更短（但可读性较差）的方法是以如下方式初始化\texttt{allWrite}
（使用\hyperref[ch8.3]{更宽松的枚举初始化}特性）：
\begin{lstlisting}
    std::filesystem::perms allWrite{0222};
\end{lstlisting}
\texttt{resize\_file()}可以用来缩减或者扩展普通文件的大小。例如：
\begin{lstlisting}
    // 清空文件：
    resize_file(file, 0);
\end{lstlisting}

\subsection{符号链接和依赖文件系统的路径转换}\label{ch20.4.5}
表\hyperref[t20.18]{文件系统路径转换}列出了所有访问实际文件系统的处理路径的操作。
当你想处理符号链接时这些操作尤其重要。使用\hyperref[ch20.3.3]{纯路径转换}开销更小但不会
访问实际的文件系统。
\begin{table}[htb]
    \centering
    \begin{tabular}{l|l}
        \hline
        \textbf{调用}                     & \textbf{效果}                           \\
        \hline
        \texttt{read\_symlink(symlink)} & 返回符号链接指向的文件                           \\
        \texttt{absolute(p)}            & 返回\texttt{p}的绝对路径（不解析符号链接）            \\
        \texttt{canonical(p)}           & 返回已存在的\texttt{p}的绝对路径（解析符号链接）         \\
        \texttt{weakly\_canonical(p)}   & 返回\texttt{p}的绝对路径（解析符号链接）             \\
        \texttt{relative(p)}            & 返回从当前目录到\texttt{p}的相对（或空）路径           \\
        \texttt{relative(p, base)}      & 返回从\texttt{base}到\texttt{p}的相对（或空）路径  \\
        \texttt{proximate(p)}           & 返回从当前目录到\texttt{p}的相对（或绝对）路径          \\
        \texttt{proximate(p, base)}     & 返回从\texttt{base}到\texttt{p}的相对（或绝对）路径 \\
        \hline
    \end{tabular}
    \caption{文件系统路径转换}
    \label{t20.18}
\end{table}

注意根据文件是否必须存在、路径是否正规化、是否解析符号链接这些函数有不同的行为。
表\hyperref[t20.19]{文件系统路径转换属性}列出了这些函数的需求和行为。
\begin{table}[htb]
    \centering
    \begin{tabular}{l|l|l|l}
        \hline
        \textbf{调用}                  & \textbf{必须存在} & \textbf{正规化} & \textbf{解析符号链接} \\
        \hline
        \texttt{read\_symlink()}     & Yes           & Yes          & Once            \\
        \texttt{absolute()}          & No            & Yes          & No              \\
        \texttt{canonical()}         & Yes           & Yes          & All             \\
        \texttt{weakly\_canonical()} & No            & Yes          & All             \\
        \texttt{relative()}          & No            & Yes          & All             \\
        \texttt{proximate()}         & No            & Yes          & All             \\
        \hline
    \end{tabular}
    \caption{文件系统路径转换属性}
    \label{t20.19}
\end{table}

下面的函数演示了处理符号链接时这些函数的使用方法和效果：
\inputcodefile{filesystem/symlink.hpp}
注意我们首先把一个可能是相对路径的路径转换成了绝对路径，
否则切换当前路径时当前路径可能会影响切换到的位置。
\hyperref[ch20.3.2]{\texttt{relative\_path()}}和\hyperref[ch20.3.3]{\texttt{lexically\_relative()}}都是
开销很小的成员函数，不会访问实际的文件系统。因此，它们会忽略符号链接。

独立函数\texttt{relative()}会访问实际的文件系统。当文件不存在时它的行为和\texttt{lexically\_relative()}一样。
然而，在创建了符号链接\texttt{ps}（指向\texttt{top}）之后，它会解析符号链接并给出一个不同的结果。

在POSIX系统上，在当前路径\texttt{"/tmp"}以参数\texttt{"top"}调用上述函数将有如下输出：
\begin{blacklisting}
    "/tmp/top"
    "tmp/top/a/x"
    "../x"
    "../x"
    "a/x"
    "../x"
    "../x"
    ps: "tmp/top/a/s" -> "/tmp/top"
    "../x"
    "a/x"
\end{blacklisting}
在Windows系统上，在当前路径\texttt{"C:/temp"}以参数\texttt{"top"}调用
上述函数会有如下输出：
\begin{blacklisting}
    "C:\\temp\\top"
    "temp\\top\\a/x"
    "..\\x"
    "..\\x"
    "a\\x"
    "..\\x"
    "..\\x"
    ps: "C:\\temp\\top\\a/s" -> "C:\\temp\\top"
    "..\\x"
    "a\\x"
\end{blacklisting}
再次注意在Windows上你需要管理员权限才能创建符号链接。

\subsection{其他文件系统操作}\label{ch20.4.6}
表\hyperref[t20.20]{其他操作}列出了所有还未提到的其他文件系统操作。
\begin{table}[htb]
    \centering
    \begin{tabular}{l|l}
        \hline
        \textbf{调用}                 & \textbf{效果}                        \\
        \hline
        \texttt{equivalent(p1, p2)} & 返回是否\texttt{p1}和\texttt{p2}指向同一个文件 \\
        \texttt{space(p)}           & 返回路径\texttt{p}的磁盘空间信息              \\
        \texttt{current\_path(p)}   & 将当前工作目录设置为\texttt{p}               \\
        \hline
    \end{tabular}
    \caption{其他操作}
    \label{t20.20}
\end{table}

\texttt{equivalent()}函数在\hyperref[ch20.3.6]{关于路径比较的小节}介绍。

\texttt{space()}的返回值是如下的结构体：
\begin{lstlisting}
    namespace std::filesystem {
        struct space_info {
            uintmax_t capacity;
            uintmax_t free;
            uintmax_t available;
        };
    }
\end{lstlisting}
因此，使用\nameref{ch1}，你可以像下面这样打印出根目录可用的磁盘空间：
\begin{lstlisting}
    auto [cap, _, avail] = std::filesystem::space("/");
    std::cout << std::fixed << std::precision(2)
              << avail/1.0e6 << " of " << cap/1.0e6 << " MB available\n\n";
\end{lstlisting}
输出可能是：
\begin{blacklisting}
    43019.82 of 150365.79 MB available
\end{blacklisting}
\texttt{current\_path()}调用将整个程序（也会应用于所有线程）
的当前目录设置为参数指示的路径。通过下面的代码，你可以切换到另一个工作目录
并在离开作用域时恢复旧的工作目录：
\begin{lstlisting}
    // 保存当前路径：
    auto currentDir{std::filesystem::current_path()};

    try {
        // 临时切换当前路径：
        std::filesystem::current_path(subdir);
        ...     // 执行一些操作
    }
    catch (...) {
        // 发生异常时恢复当前路径：
        std::filesystem::current_path(currentDir);
        throw;  // 重新抛出
    }
    // 没有异常时恢复当前路径：
    std::filesystem::current_path(currentDir);
\end{lstlisting}


\section{遍历目录}\label{ch20.5}
文件系统库的一个关键作用就是遍历一个文件系统（子）树中的所有文件。

能做到这一点的最快捷的方法就是使用范围\texttt{for}循环。
你可以遍历一个目录中的所有文件：
\begin{lstlisting}
    for (const auto& e : std::filesystem::directory_iterator(dir)) {
        std::cout << e.path() << '\n';
    }
\end{lstlisting}
或者递归遍历一个文件系统（子）树：
\begin{lstlisting}
    for (const auto& e : std::filesystem::recursive_directory_iterator(dir)) {
        std::cout << e.path() << '\n';
    }
\end{lstlisting}
传入的参数\texttt{dir}可以是一个\texttt{path}也可以是其他可以隐式转换为路径的类型
（特指各种形式的字符串）。\label{遍历.}

注意\texttt{e.path()}返回包含遍历起点目录的文件名。因此，如果我们遍历\texttt{"."}
里的一个文件\texttt{file.txt}，文件名将是\texttt{./file.txt}或者\texttt{.\textbackslash file.txt}。

另外，路径被写入输出流时会带有双引号，所以输出时将变为\texttt{"./file.txt"}
或者\texttt{".\textbackslash \textbackslash file.txt"}。
因此，就像之前的\hyperref[ch20.1.3.5]{最开始的例子}，下面的循环可移植性更强：
\begin{lstlisting}
    for (const auto& e : std::filesystem::directory_iterator(dir)) {
        std::cout << e.path().lexically_normal().string() << '\n';
    }
\end{lstlisting}
为了遍历当前目录，传递\texttt{"."}作为当前目录而不是\texttt{""}。
在Windows上传递一个空路径是可以的但不可移植。

\subsubsection{目录迭代器表示一个范围}
你可能会很惊讶你可以把一个迭代器传递给范围\texttt{for}循环，因为正常情况下应该传递一个范围。

技巧在于\texttt{directory\_iterator}和\texttt{recursive\_directory\_iterator}
都有提供全局的\texttt{begin()}和\\
\texttt{end()}函数重载：
\begin{itemize}
    \item \texttt{begin()}返回迭代器自身。
    \item \texttt{end()}返回尾后迭代器，可以使用默认构造函数创建。
\end{itemize}
因此，你可以像下面这样遍历：
\begin{lstlisting}
    std::filesystem::directory_iterator di{p};
    for (auto pos = begin(di); pos != end(di); ++pos) {
        std::cout << pos->path() << '\n';
    }
\end{lstlisting}
或者像下面这样：
\begin{lstlisting}
    for (std::filesystem::directory_iterator pos{p};
         pos != std::filesystem::directory_iterator{};
         ++pos) {
        std::cout << pos->path() << '\n';
    }
\end{lstlisting}

\subsubsection{目录迭代器选项}
当遍历目录时，你可以传递\texttt{directory\_options}类型的值，这些值在
表\hyperref[t20.21]{目录迭代器选项}中列出。
这个类型是一个位域的枚举类型，定义在命名空间
\texttt{std::filesystem}中。
\begin{table}[htb]
    \centering
    \begin{tabular}{l|l}
        \hline
        \texttt{directory\_options}         & \textbf{效果}     \\
        \hline
        \texttt{none}                       & 默认情况（值为0）       \\
        \texttt{follow\_directory\_symlink} & 解析符号链接（而不是跳过它们） \\
        \texttt{skip\_permission\_denied}   & 当权限不足时跳过目录      \\
        \hline
    \end{tabular}
    \caption{目录迭代器选项}
    \label{t20.21}
\end{table}

默认行为是不解析符号链接并在没有权限访问（子）目录时抛出异常。
使用\texttt{skip\_permission\_denied}选项可以忽略那些没有权限访问的目录。

\hyperref[ch20.1.3]{\emph{filesystem/createfiles.cpp}}展示了
\texttt{follow\_directory\_symlink}选项的一个应用。

\subsection{目录项}
目录迭代器的元素类型是\texttt{std::filesystem::directory\_entry}。
因此，如果目录迭代器有效的话，使用\texttt{operator*()}将会返回这个类型。
这意味着范围\texttt{for}循环的正确类型如下所示：
\begin{lstlisting}
    for (const std::filesystem::directory_entry& e : std::filesystem::directory_iterator(p)) {
        std::cout << e.path() << '\n';
    }
\end{lstlisting}
目录项包含一个\texttt{path}对象和一些附加的属性，例如硬链接数量、文件状态、文件大小、上次
修改时间、是否是符号链接、如果是的话它指向的实际路径等。

注意这个迭代器是输入迭代器。因为目录项随时都可能变化，所以重复遍历一个目录可能会得到不同的结果，
\hyperref[ch22.6.1.4]{在并行算法中使用目录迭代器}时必须考虑到这一点。

表\hyperref[t20.22]{目录项操作}列出了目录项\texttt{e}的所有操作。
它们基本都是一些查询\nameref{ch20.4.1}、获取\nameref{ch20.4.2}、
检查\nameref{ch20.4.3}、\hyperref[ch20.3.6]{比较}路径的操作。
\begin{table}[htb]
    \centering
    \begin{tabular}{l|l}
        \hline
        \textbf{调用}                      & \textbf{效果}                                \\
        \hline
        \texttt{e.path()}                & 返回当前目录项的文件系统路径                             \\
        \texttt{e.exists()}              & 返回文件是否存在                                   \\
        \texttt{e.is\_regular\_file()}   & 返回是否文件存在并且是普通文件                            \\
        \texttt{e.is\_directory()}       & 返回是否文件存在并且是目录                              \\
        \texttt{e.is\_symlink()}         & 返回是否文件存在并且是符号链接                            \\
        \texttt{e.is\_other()}           & 返回是否文件存在并且不是普通文件或目录或符号链接                   \\
        \texttt{e.is\_block\_file()}     & 返回是否文件存在并且是块特殊文件                           \\
        \texttt{e.is\_character\_file()} & 返回是否文件存在并且是字符特殊文件                          \\
        \texttt{e.is\_fifo()}            & 返回是否文件存在并且是FIFO或者管道文件                      \\
        \texttt{e.is\_socket()}          & 返回是否文件存在并且是套接字                             \\
        \texttt{e.file\_size()}          & 返回文件大小                                     \\
        \texttt{e.hard\_link\_count()}   & 返回硬链接数量                                    \\
        \texttt{e.last\_write\_time()}   & 返回最后一次修改时间                                 \\
        \texttt{e.status()}              & 返回文件\texttt{p}的\hyperref[ch20.4.2]{状态}     \\
        \texttt{e.symlink\_status()}     & 返回文件\texttt{p}的\nameref{ch20.4.2}（解析符号链接）  \\
        \texttt{e1 == e2}                & 返回两个目录项的路径是否相等                             \\
        \texttt{e1 != e2}                & 返回两个目录项的路径是否不相等                            \\
        \texttt{e1 < e2}                 & 返回是否一个目录项的路径小于另一个                          \\
        \texttt{e1 <= e2}                & 返回是否一个目录项的路径小于等于另一个                        \\
        \texttt{e1 >= e2}                & 返回是否一个目录项的路径大于等于另一个                        \\
        \texttt{e1 > e2}                 & 返回是否一个目录项的路径大于另一个                          \\
        \texttt{e.assign(p)}             & 用\texttt{p}替换\texttt{e}的路径并更新目录项的所有属性      \\
        \texttt{e.replace\_filename(p)}  & 用\texttt{p}替换\texttt{e}的路径中的文件名并更新目录项的所有属性 \\
        \texttt{e.refresh()}             & 更新该目录项所有缓存的属性                              \\
        \hline
    \end{tabular}
    \caption{目录项操作}
    \label{t20.22}
\end{table}

\texttt{assign()}和\texttt{replace\_filename()}会
调用\hyperref[ch20.3.5]{相应的修改路径操作}，
但不会修改底层文件系统中的文件。

\subsubsection{目录项缓存}
鼓励实现\emph{缓存}额外的文件属性来避免使用目录项时额外的文件系统访问开销。
然而，实现并不是必须缓存数据，因为这样一些操作的开销会变大。
\footnote{事实上，C++17文件系统库在g++ v9中的测试实现只缓存文件类型，
而不缓存文件大小（等到库正式发布时这一点可能会改变。）}

因为所有的值通常会被缓存，因此这些调用通常开销很小：
\begin{lstlisting}
    for (const auto& e : std::filesystem::directory_iterator{"."}) {
        auto t = e.last_write_time();   // 通常开销很小
        ...
    }
\end{lstlisting}
不管是否有缓存，但在多用户或者多进程的操作系统中，所有这些迭代都可能返回不再有效的数据。
文件的大小和内容可能改变、文件可能被删除或者替换（因此，文件的类型也有可能改变）、
权限也可能被修改。

在这种情况下，你可以要求更新目录项持有的数据：
\begin{lstlisting}
    for (const auto& e : std::filesystem::directory_iterator{"."}) {
        ...             // 数据已经失效
        e.refresh();    // 刷新文件的缓存数据
        if (e.exists()) {
            auto t = e.last_write_time();
            ...
        }
    }
\end{lstlisting}
或者，你可以总是查询当前的状态：
\begin{lstlisting}
    for (const auto& e : std::filesystem::directory_iterator{"."}) {
        ...             // 数据已经失效
        if (exists(e.path()) {
            auto t = last_write_time(e.path());
            ...
        }
    }
\end{lstlisting}


\section{后记}
文件系统库一开始作为Boost库在Beman Dawes的带领下开发了很多年。
在2014年，它第一次成为了正式的测试标准：\emph{File System Technical Specification}
（见\url{https://wg21.link/n4100})。

Beman Dawes的\url{https://wg21.link/p0218r0}提案使得\emph{File System Technical Specification}
被标准库采纳。计算相对路径的支持由Beman Dawes、Nicolai Josuttis、Jamie Allsop在
\url{https://wg21.link/p0219r1}中添加。Beman Dawes在\url{https://wg21.link/p0317r1}中、
Nicolai Josuttis在\url{https://wg21.link/p0392r0}中、Jason Liu和Hubert Tong在\url{https://wg21.link/p0430r2}中、
尤其是文件系统小组
（Beman Dawes、S. Davis Herring、Nicolai Josuttis、 Jason Liu、Billy O’Neal、P.J. Plauger、Jonathan Wakely）
在\url{https://wg21.link/p0492r2}中修正了很多小错误。

C++17标准发布以后，Nicolai Josuttis在\url{https://wg21.link/p1164r1}提案中作为C++17的缺陷修正了
\texttt{make\_directory()}的行为。
